Reformat using black

This commit is contained in:
Deniz Şafak
2025-11-19 01:54:11 +03:00
parent 43f5589a9d
commit bb5c4204de
8 changed files with 572 additions and 311 deletions
+2
View File
@@ -377,6 +377,7 @@ class HandlerDialog(QDialog):
# Include replace_single_newlines in cache key since it affects text cleaning
from abogen.utils import load_config
cfg = load_config()
replace_single_newlines = cfg.get("replace_single_newlines", False)
@@ -2240,6 +2241,7 @@ class HandlerDialog(QDialog):
if metadata.get("cover_image"):
try:
import uuid
cache_dir = get_user_cache_path()
cover_path = os.path.join(cache_dir, f"cover_{uuid.uuid4()}.jpg")
cover_path = os.path.normpath(cover_path)
+1 -3
View File
@@ -2,9 +2,7 @@ from abogen.utils import get_version
# Program Information
PROGRAM_NAME = "abogen"
PROGRAM_DESCRIPTION = (
"Generate audiobooks from EPUBs, PDFs, text and subtitles with synchronized captions."
)
PROGRAM_DESCRIPTION = "Generate audiobooks from EPUBs, PDFs, text and subtitles with synchronized captions."
GITHUB_URL = "https://github.com/denizsafak/abogen"
VERSION = get_version()
+377 -147
View File
@@ -32,9 +32,9 @@ import platform
def clean_subtitle_text(text):
"""Remove chapter markers and metadata tags from subtitle text."""
# Remove metadata tags
text = re.sub(r'<<METADATA_[^:]+:[^>]*>>', '', text)
text = re.sub(r"<<METADATA_[^:]+:[^>]*>>", "", text)
# Remove chapter markers
text = re.sub(r'<<CHAPTER_MARKER:[^>]*>>', '', text)
text = re.sub(r"<<CHAPTER_MARKER:[^>]*>>", "", text)
return text.strip()
@@ -49,43 +49,46 @@ def parse_srt_file(file_path):
List of tuples: [(start_time_seconds, end_time_seconds, text), ...]
"""
encoding = detect_encoding(file_path)
with open(file_path, 'r', encoding=encoding, errors='replace') as f:
with open(file_path, "r", encoding=encoding, errors="replace") as f:
content = f.read()
# Split by double newlines to get individual subtitle blocks
blocks = re.split(r'\n\s*\n', content.strip())
blocks = re.split(r"\n\s*\n", content.strip())
subtitles = []
for block in blocks:
if not block.strip():
continue
lines = block.strip().split('\n')
lines = block.strip().split("\n")
if len(lines) < 3:
continue
# First line is index, second line is timestamp, rest is text
try:
timestamp_line = lines[1]
match = re.match(r'(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})', timestamp_line)
match = re.match(
r"(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})",
timestamp_line,
)
if not match:
continue
start_str = match.group(1)
end_str = match.group(2)
text = '\n'.join(lines[2:])
text = "\n".join(lines[2:])
# Convert timestamp to seconds
def time_to_seconds(t):
h, m, s_ms = t.split(':')
s, ms = s_ms.split(',')
h, m, s_ms = t.split(":")
s, ms = s_ms.split(",")
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
start_sec = time_to_seconds(start_str)
end_sec = time_to_seconds(end_str)
# Clean text of any styling tags
text = re.sub(r'<[^>]+>', '', text)
text = re.sub(r"<[^>]+>", "", text)
# Remove chapter markers and metadata tags
text = clean_subtitle_text(text)
@@ -108,23 +111,23 @@ def parse_vtt_file(file_path):
List of tuples: [(start_time_seconds, end_time_seconds, text), ...]
"""
encoding = detect_encoding(file_path)
with open(file_path, 'r', encoding=encoding, errors='replace') as f:
with open(file_path, "r", encoding=encoding, errors="replace") as f:
content = f.read()
# Remove WEBVTT header and any style/note blocks
content = re.sub(r'^WEBVTT.*?\n', '', content, flags=re.MULTILINE)
content = re.sub(r'STYLE\s*\n.*?(?=\n\n|$)', '', content, flags=re.DOTALL)
content = re.sub(r'NOTE\s*\n.*?(?=\n\n|$)', '', content, flags=re.DOTALL)
content = re.sub(r"^WEBVTT.*?\n", "", content, flags=re.MULTILINE)
content = re.sub(r"STYLE\s*\n.*?(?=\n\n|$)", "", content, flags=re.DOTALL)
content = re.sub(r"NOTE\s*\n.*?(?=\n\n|$)", "", content, flags=re.DOTALL)
# Split by double newlines to get individual subtitle blocks
blocks = re.split(r'\n\s*\n', content.strip())
blocks = re.split(r"\n\s*\n", content.strip())
subtitles = []
for block in blocks:
if not block.strip():
continue
lines = block.strip().split('\n')
lines = block.strip().split("\n")
if len(lines) < 2:
continue
@@ -133,10 +136,10 @@ def parse_vtt_file(file_path):
text_start_idx = 0
# Check if first line is timestamp
if '-->' in lines[0]:
if "-->" in lines[0]:
timestamp_line = lines[0]
text_start_idx = 1
elif len(lines) > 1 and '-->' in lines[1]:
elif len(lines) > 1 and "-->" in lines[1]:
timestamp_line = lines[1]
text_start_idx = 2
else:
@@ -144,24 +147,24 @@ def parse_vtt_file(file_path):
try:
# VTT format: 00:00:00.000 --> 00:00:05.000 or 00:00.000 --> 00:05.000
match = re.match(r'([\d:.]+)\s*-->\s*([\d:.]+)', timestamp_line)
match = re.match(r"([\d:.]+)\s*-->\s*([\d:.]+)", timestamp_line)
if not match:
continue
start_str = match.group(1)
end_str = match.group(2)
text = '\n'.join(lines[text_start_idx:])
text = "\n".join(lines[text_start_idx:])
# Convert timestamp to seconds
def time_to_seconds(t):
parts = t.split(':')
parts = t.split(":")
if len(parts) == 3: # HH:MM:SS.mmm
h, m, s = parts
s, ms = s.split('.')
s, ms = s.split(".")
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
elif len(parts) == 2: # MM:SS.mmm
m, s = parts
s, ms = s.split('.')
s, ms = s.split(".")
return int(m) * 60 + int(s) + int(ms) / 1000.0
return 0
@@ -169,8 +172,8 @@ def parse_vtt_file(file_path):
end_sec = time_to_seconds(end_str)
# Clean text of any styling tags and cue settings
text = re.sub(r'<[^>]+>', '', text)
text = re.sub(r'{[^}]+}', '', text) # Remove voice tags
text = re.sub(r"<[^>]+>", "", text)
text = re.sub(r"{[^}]+}", "", text) # Remove voice tags
# Remove chapter markers and metadata tags
text = clean_subtitle_text(text)
@@ -186,12 +189,14 @@ def detect_timestamps_in_text(file_path):
"""Detect if text file contains timestamp markers (HH:MM:SS or HH:MM:SS,ms format) on separate lines."""
try:
encoding = detect_encoding(file_path)
with open(file_path, 'r', encoding=encoding, errors='replace') as f:
lines = [line.strip() for line in f.readlines()[:50] if line.strip()] # Check first 50 non-empty lines
with open(file_path, "r", encoding=encoding, errors="replace") as f:
lines = [
line.strip() for line in f.readlines()[:50] if line.strip()
] # Check first 50 non-empty lines
# Count lines that are ONLY timestamps (no other text)
# Supports HH:MM:SS or HH:MM:SS,ms format
timestamp_pattern = r'^(\d{1,2}:\d{2}:\d{2}(?:,\d{1,3})?)$'
timestamp_pattern = r"^(\d{1,2}:\d{2}:\d{2}(?:,\d{1,3})?)$"
timestamp_lines = sum(1 for line in lines if re.match(timestamp_pattern, line))
# Must have at least 2 timestamp-only lines and they should be >5% of total lines
@@ -204,23 +209,23 @@ def parse_timestamp_text_file(file_path):
"""Parse text file with timestamps. Returns list of (start_time, end_time, text) tuples.
Supports HH:MM:SS or HH:MM:SS,ms format. Returns time in seconds as float."""
encoding = detect_encoding(file_path)
with open(file_path, 'r', encoding=encoding, errors='replace') as f:
with open(file_path, "r", encoding=encoding, errors="replace") as f:
content = f.read()
# Split by timestamp pattern (supports HH:MM:SS or HH:MM:SS,ms)
pattern = r'^(\d{1,2}:\d{2}:\d{2}(?:,\d{1,3})?)$'
lines = content.split('\n')
pattern = r"^(\d{1,2}:\d{2}:\d{2}(?:,\d{1,3})?)$"
lines = content.split("\n")
def parse_time(time_str):
"""Convert HH:MM:SS or HH:MM:SS,ms to seconds as float."""
if ',' in time_str:
time_part, ms_part = time_str.split(',')
parts = time_part.split(':')
if "," in time_str:
time_part, ms_part = time_str.split(",")
parts = time_part.split(":")
seconds = int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2])
milliseconds = int(ms_part.ljust(3, '0')) # Pad to 3 digits
milliseconds = int(ms_part.ljust(3, "0")) # Pad to 3 digits
return seconds + milliseconds / 1000.0
else:
parts = time_str.split(':')
parts = time_str.split(":")
return float(int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2]))
entries = []
@@ -233,12 +238,12 @@ def parse_timestamp_text_file(file_path):
if match:
# Save previous entry
if current_time is not None and current_text:
text = '\n'.join(current_text).strip()
text = "\n".join(current_text).strip()
if text:
entries.append((current_time, text))
elif current_time is None and pre_timestamp_text:
# First timestamp found, save pre-timestamp text with time 0
text = '\n'.join(pre_timestamp_text).strip()
text = "\n".join(pre_timestamp_text).strip()
if text:
entries.append((0.0, text))
pre_timestamp_text = []
@@ -255,12 +260,12 @@ def parse_timestamp_text_file(file_path):
# Save last entry
if current_time is not None and current_text:
text = '\n'.join(current_text).strip()
text = "\n".join(current_text).strip()
if text:
entries.append((current_time, text))
elif not entries and pre_timestamp_text:
# No timestamps found at all, treat entire file as starting at 0
text = '\n'.join(pre_timestamp_text).strip()
text = "\n".join(pre_timestamp_text).strip()
if text:
entries.append((0.0, text))
@@ -287,7 +292,7 @@ def parse_ass_file(file_path):
List of tuples: [(start_time_seconds, end_time_seconds, text), ...]
"""
encoding = detect_encoding(file_path)
with open(file_path, 'r', encoding=encoding, errors='replace') as f:
with open(file_path, "r", encoding=encoding, errors="replace") as f:
lines = f.readlines()
subtitles = []
@@ -297,50 +302,56 @@ def parse_ass_file(file_path):
for line in lines:
line = line.strip()
if line.startswith('[Events]'):
if line.startswith("[Events]"):
in_events = True
continue
if line.startswith('[') and in_events:
if line.startswith("[") and in_events:
# New section, stop processing
break
if in_events and line.startswith('Format:'):
if in_events and line.startswith("Format:"):
# Parse format line to know column positions
parts = line.split(':', 1)[1].strip().split(',')
parts = line.split(":", 1)[1].strip().split(",")
for i, part in enumerate(parts):
format_indices[part.strip().lower()] = i
continue
if in_events and (line.startswith('Dialogue:') or line.startswith('Comment:')):
if line.startswith('Comment:'):
if in_events and (line.startswith("Dialogue:") or line.startswith("Comment:")):
if line.startswith("Comment:"):
continue # Skip comments
parts = line.split(':', 1)[1].strip().split(',', len(format_indices) - 1)
parts = line.split(":", 1)[1].strip().split(",", len(format_indices) - 1)
if 'start' in format_indices and 'end' in format_indices and 'text' in format_indices:
start_str = parts[format_indices['start']].strip()
end_str = parts[format_indices['end']].strip()
text = parts[format_indices['text']].strip()
if (
"start" in format_indices
and "end" in format_indices
and "text" in format_indices
):
start_str = parts[format_indices["start"]].strip()
end_str = parts[format_indices["end"]].strip()
text = parts[format_indices["text"]].strip()
# Convert timestamp to seconds (ASS format: H:MM:SS.CS where CS is centiseconds)
def ass_time_to_seconds(t):
parts = t.split(':')
parts = t.split(":")
if len(parts) == 3:
h, m, s = parts
s_parts = s.split('.')
s_parts = s.split(".")
seconds = float(s_parts[0])
centiseconds = float(s_parts[1]) if len(s_parts) > 1 else 0
return int(h) * 3600 + int(m) * 60 + seconds + centiseconds / 100.0
return (
int(h) * 3600 + int(m) * 60 + seconds + centiseconds / 100.0
)
return 0
start_sec = ass_time_to_seconds(start_str)
end_sec = ass_time_to_seconds(end_str)
# Clean text of ASS styling tags
text = re.sub(r'\{[^}]+\}', '', text) # Remove {tags}
text = re.sub(r'\\N', '\n', text) # Convert \N to newline
text = re.sub(r'\\n', '\n', text) # Convert \n to newline
text = re.sub(r"\{[^}]+\}", "", text) # Remove {tags}
text = re.sub(r"\\N", "\n", text) # Convert \N to newline
text = re.sub(r"\\n", "\n", text) # Convert \n to newline
# Remove chapter markers and metadata tags
text = clean_subtitle_text(text)
@@ -418,6 +429,7 @@ def sanitize_name_for_os(name, is_folder=True):
class CountdownDialog(QDialog):
"""Base dialog with auto-accept countdown functionality"""
def __init__(self, title, countdown_seconds, parent=None):
super().__init__(parent)
self.setWindowTitle(title)
@@ -435,7 +447,9 @@ class CountdownDialog(QDialog):
def add_countdown_and_buttons(self):
"""Add countdown label and OK button - call this after adding custom content"""
self.countdown_label = QLabel(f"Auto-accepting in {self.countdown_seconds} seconds...")
self.countdown_label = QLabel(
f"Auto-accepting in {self.countdown_seconds} seconds..."
)
self.countdown_label.setStyleSheet(f"color: {COLORS['GREEN']};")
self.layout.addWidget(self.countdown_label)
@@ -450,7 +464,9 @@ class CountdownDialog(QDialog):
def _on_timer_tick(self):
self.countdown_seconds -= 1
if self.countdown_seconds > 0:
self.countdown_label.setText(f"Auto-accepting in {self.countdown_seconds} seconds...")
self.countdown_label.setText(
f"Auto-accepting in {self.countdown_seconds} seconds..."
)
else:
self._timer.stop()
self._button_box.accepted.emit()
@@ -469,7 +485,9 @@ class ChapterOptionsDialog(CountdownDialog):
def __init__(self, chapter_count, parent=None):
super().__init__("Chapter Options", CHAPTER_OPTIONS_COUNTDOWN, parent)
self.layout.addWidget(QLabel(f"Detected {chapter_count} chapters in the text file."))
self.layout.addWidget(
QLabel(f"Detected {chapter_count} chapters in the text file.")
)
self.layout.addWidget(QLabel("How would you like to process these chapters?"))
self.save_separately_checkbox = QCheckBox("Save each chapter separately")
@@ -478,7 +496,9 @@ class ChapterOptionsDialog(CountdownDialog):
self.save_separately_checkbox.setChecked(False)
self.merge_at_end_checkbox.setChecked(True)
self.save_separately_checkbox.stateChanged.connect(self.update_merge_checkbox_state)
self.save_separately_checkbox.stateChanged.connect(
self.update_merge_checkbox_state
)
self.layout.addWidget(self.save_separately_checkbox)
self.layout.addWidget(self.merge_at_end_checkbox)
@@ -492,7 +512,8 @@ class ChapterOptionsDialog(CountdownDialog):
def get_options(self):
return {
"save_chapters_separately": self.save_separately_checkbox.isChecked(),
"merge_chapters_at_end": self.merge_at_end_checkbox.isChecked() and self.merge_at_end_checkbox.isEnabled(),
"merge_chapters_at_end": self.merge_at_end_checkbox.isChecked()
and self.merge_at_end_checkbox.isEnabled(),
}
@@ -507,9 +528,13 @@ class TimestampDetectionDialog(QDialog):
layout = QVBoxLayout(self)
layout.addWidget(QLabel("This file contains timestamps in HH:MM:SS format."))
layout.addWidget(QLabel("Do you want to use these timestamps for precise audio timing?"))
layout.addWidget(
QLabel("Do you want to use these timestamps for precise audio timing?")
)
yes_label = QLabel("• Yes: Generate audio that matches each timestamp (subtitle mode will be ignored)")
yes_label = QLabel(
"• Yes: Generate audio that matches each timestamp (subtitle mode will be ignored)"
)
yes_label.setStyleSheet(f"color: {COLORS['BLUE_BORDER_HOVER']};")
layout.addWidget(yes_label)
@@ -518,7 +543,9 @@ class TimestampDetectionDialog(QDialog):
layout.addWidget(no_label)
# Countdown label
self.countdown_label = QLabel(f"Auto-accepting in {self.countdown_seconds} seconds...")
self.countdown_label = QLabel(
f"Auto-accepting in {self.countdown_seconds} seconds..."
)
self.countdown_label.setStyleSheet(f"color: {COLORS['GREEN']};")
layout.addWidget(self.countdown_label)
@@ -539,7 +566,9 @@ class TimestampDetectionDialog(QDialog):
def _on_timer_tick(self):
self.countdown_seconds -= 1
if self.countdown_seconds > 0:
self.countdown_label.setText(f"Auto-accepting in {self.countdown_seconds} seconds...")
self.countdown_label.setText(
f"Auto-accepting in {self.countdown_seconds} seconds..."
)
else:
self._timer.stop()
self._set_result(True)
@@ -565,18 +594,18 @@ class ConversionThread(QThread):
# Languages that should not use split pattern (better handled by Kokoro internally)
# These languages have different text segmentation rules (no spaces, character-based, etc.)
NO_SPLIT_LANGUAGES = {'z', 'j'} # Chinese, Japanese
NO_SPLIT_LANGUAGES = {"z", "j"} # Chinese, Japanese
# Language-specific punctuation patterns for subtitle splitting
LANGUAGE_PUNCTUATION = {
'z': {
'sentence': r"[。!?]", # Chinese: period, exclamation, question
'comma': r"[。!?、,]", # Chinese: includes enumeration comma and comma
"z": {
"sentence": r"[。!?]", # Chinese: period, exclamation, question
"comma": r"[。!?、,]", # Chinese: includes enumeration comma and comma
},
"j": {
"sentence": r"[。!?]", # Japanese: period, exclamation, question
"comma": r"[。!?、,]", # Japanese: includes enumeration comma and comma
},
'j': {
'sentence': r"[。!?]", # Japanese: period, exclamation, question
'comma': r"[。!?、,]", # Japanese: includes enumeration comma and comma
}
}
def __init__(
@@ -627,7 +656,9 @@ class ConversionThread(QThread):
self.max_subtitle_words = 50 # Default value, will be overridden from GUI
self.silence_duration = 2.0 # Default value, will be overridden from GUI
# Set split pattern based on language - some languages handle splitting better internally
self.split_pattern = None if lang_code in self.NO_SPLIT_LANGUAGES else self.DEFAULT_SPLIT_PATTERN
self.split_pattern = (
None if lang_code in self.NO_SPLIT_LANGUAGES else self.DEFAULT_SPLIT_PATTERN
)
def _stream_audio_in_chunks(
self, segments, process_func, progress_prefix="Processing"
@@ -741,15 +772,19 @@ class ConversionThread(QThread):
is_subtitle_input = False
if not self.is_direct_text and self.file_name:
file_ext = os.path.splitext(self.file_name)[1].lower()
if file_ext in ['.srt', '.ass', '.vtt']:
if file_ext in [".srt", ".ass", ".vtt"]:
is_subtitle_input = True
# Display subtitle-specific options if processing subtitle file
if is_subtitle_input:
if getattr(self, 'use_silent_gaps', False):
if getattr(self, "use_silent_gaps", False):
self.log_updated.emit("- Use silent gaps: Yes")
speed_method = getattr(self, 'subtitle_speed_method', 'tts')
method_label = "TTS Regeneration" if speed_method == "tts" else "FFmpeg Time-stretch"
speed_method = getattr(self, "subtitle_speed_method", "tts")
method_label = (
"TTS Regeneration"
if speed_method == "tts"
else "FFmpeg Time-stretch"
)
self.log_updated.emit(f"- Speed adjustment method: {method_label}")
# Display save_chapters_separately flag if it's set
@@ -802,23 +837,25 @@ class ConversionThread(QThread):
is_timestamp_text = False
if not self.is_direct_text and self.file_name:
file_ext = os.path.splitext(self.file_name)[1].lower()
if file_ext in ['.srt', '.ass', '.vtt']:
if file_ext in [".srt", ".ass", ".vtt"]:
is_subtitle_file = True
self.log_updated.emit(f"\nDetected subtitle file format: {file_ext}")
elif file_ext == '.txt' and detect_timestamps_in_text(self.file_name):
self.log_updated.emit(
f"\nDetected subtitle file format: {file_ext}"
)
elif file_ext == ".txt" and detect_timestamps_in_text(self.file_name):
is_timestamp_text = True
self.log_updated.emit("\nDetected timestamps in text file")
# Signal to ask user (-1 indicates timestamp detection)
self.chapters_detected.emit(-1)
# Wait for user response
while not hasattr(self, '_timestamp_response'):
while not hasattr(self, "_timestamp_response"):
if self.cancel_requested:
self.conversion_finished.emit("Cancelled", None)
return
time.sleep(0.1)
if not self._timestamp_response:
is_timestamp_text = False
delattr(self, '_timestamp_response')
delattr(self, "_timestamp_response")
# Process subtitle files separately
if is_subtitle_file or is_timestamp_text:
@@ -1650,16 +1687,18 @@ class ConversionThread(QThread):
subtitles = parse_timestamp_text_file(self.file_name)
else:
file_ext = os.path.splitext(self.file_name)[1].lower()
if file_ext == '.srt':
if file_ext == ".srt":
subtitles = parse_srt_file(self.file_name)
elif file_ext == '.vtt':
elif file_ext == ".vtt":
subtitles = parse_vtt_file(self.file_name)
else:
subtitles = parse_ass_file(self.file_name)
if not subtitles:
self.log_updated.emit(("No valid subtitle entries found.", "red"))
self.conversion_finished.emit(("No subtitle entries to process.", "red"), None)
self.conversion_finished.emit(
("No subtitle entries to process.", "red"), None
)
return
self.log_updated.emit(f"\nFound {len(subtitles)} subtitle entries")
@@ -1667,12 +1706,20 @@ class ConversionThread(QThread):
# Setup output paths
base_name = os.path.splitext(os.path.basename(base_path))[0]
sanitized_base_name = sanitize_name_for_os(base_name, is_folder=True)
parent_dir = (user_desktop_dir() if self.save_option == "Save to Desktop"
else os.path.dirname(base_path) if self.save_option == "Save next to input file"
else self.output_folder or os.getcwd())
parent_dir = (
user_desktop_dir()
if self.save_option == "Save to Desktop"
else (
os.path.dirname(base_path)
if self.save_option == "Save next to input file"
else self.output_folder or os.getcwd()
)
)
if not os.path.exists(parent_dir):
self.log_updated.emit((f"Output folder does not exist: {parent_dir}", "red"))
self.log_updated.emit(
(f"Output folder does not exist: {parent_dir}", "red")
)
return
# Find unique filename
@@ -1680,23 +1727,46 @@ class ConversionThread(QThread):
allowed_exts = set(SUPPORTED_SOUND_FORMATS + SUPPORTED_SUBTITLE_FORMATS)
while True:
suffix = f"_{counter}" if counter > 1 else ""
if not any(os.path.splitext(f)[0] == f"{sanitized_base_name}{suffix}"
if not any(
os.path.splitext(f)[0] == f"{sanitized_base_name}{suffix}"
and os.path.splitext(f)[1][1:].lower() in allowed_exts
for f in os.listdir(parent_dir)):
for f in os.listdir(parent_dir)
):
break
counter += 1
base_filepath_no_ext = os.path.join(parent_dir, f"{sanitized_base_name}{suffix}")
base_filepath_no_ext = os.path.join(
parent_dir, f"{sanitized_base_name}{suffix}"
)
merged_out_path = f"{base_filepath_no_ext}.{self.output_format}"
rate = 24000
# Setup audio output
merged_out_file, ffmpeg_proc = None, None
if self.output_format in ["wav", "mp3", "flac"]:
merged_out_file = sf.SoundFile(merged_out_path, "w", samplerate=rate, channels=1, format=self.output_format)
merged_out_file = sf.SoundFile(
merged_out_path,
"w",
samplerate=rate,
channels=1,
format=self.output_format,
)
else:
static_ffmpeg.add_paths()
cmd = ["ffmpeg", "-y", "-thread_queue_size", "32768", "-f", "f32le", "-ar", str(rate), "-ac", "1", "-i", "pipe:0"]
cmd = [
"ffmpeg",
"-y",
"-thread_queue_size",
"32768",
"-f",
"f32le",
"-ar",
str(rate),
"-ac",
"1",
"-i",
"pipe:0",
]
if self.output_format == "m4b":
metadata_options, cover_path = (
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
@@ -1730,7 +1800,9 @@ class ConversionThread(QThread):
elif self.output_format == "opus":
cmd.extend(["-c:a", "libopus", "-b:a", "24000"])
else:
self.log_updated.emit((f"Unsupported output format: {self.output_format}", "red"))
self.log_updated.emit(
(f"Unsupported output format: {self.output_format}", "red")
)
return
cmd.append(merged_out_path)
ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
@@ -1744,23 +1816,42 @@ class ConversionThread(QThread):
if "ass" in subtitle_format:
# Write ASS header
subtitle_file.write("[Script Info]\nTitle: Generated by Abogen\nScriptType: v4.00+\n\n")
subtitle_file.write(
"[Script Info]\nTitle: Generated by Abogen\nScriptType: v4.00+\n\n"
)
if self.subtitle_mode == "Sentence + Highlighting":
subtitle_file.write("[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n")
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")
subtitle_file.write("[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
subtitle_file.write(
"[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
)
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"
)
subtitle_file.write(
"[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
)
is_narrow = subtitle_format in ("ass_narrow", "ass_centered_narrow")
is_centered = subtitle_format in ("ass_centered_wide", "ass_centered_narrow")
is_centered = subtitle_format in (
"ass_centered_wide",
"ass_centered_narrow",
)
margin = "90" if is_narrow else ""
alignment = "{\\an5}" if is_centered else ""
# Load voice
loaded_voice = get_new_voice(tts, self.voice, self.use_gpu) if "*" in self.voice else self.voice
loaded_voice = (
get_new_voice(tts, self.voice, self.use_gpu)
if "*" in self.voice
else self.voice
)
# Calculate initial audio buffer size from timed subtitles only
max_end_time = max((end for _, end, _ in subtitles if end is not None), default=0)
audio_buffer = self.np.zeros(int(max_end_time * rate) + rate, dtype="float32")
max_end_time = max(
(end for _, end, _ in subtitles if end is not None), default=0
)
audio_buffer = self.np.zeros(
int(max_end_time * rate) + rate, dtype="float32"
)
# Process each subtitle and mix into buffer
self.etr_start_time = time.time()
@@ -1768,7 +1859,8 @@ class ConversionThread(QThread):
for idx, (start_time, end_time, text) in enumerate(subtitles, 1):
if self.cancel_requested:
if subtitle_file: subtitle_file.close()
if subtitle_file:
subtitle_file.close()
self.conversion_finished.emit("Cancelled", None)
return
@@ -1776,35 +1868,72 @@ class ConversionThread(QThread):
replace_nl = getattr(self, "replace_single_newlines", False)
processed_text = text.replace("\n", " ") if replace_nl else text
use_gaps = getattr(self, "use_silent_gaps", False)
next_start = subtitles[idx][0] if (use_gaps and idx < len(subtitles)) else float("inf")
next_start = (
subtitles[idx][0]
if (use_gaps and idx < len(subtitles))
else float("inf")
)
subtitle_duration = None if end_time is None else end_time - start_time
h1, m1, s1 = int(start_time // 3600), int(start_time % 3600 // 60), int(start_time % 60)
h1, m1, s1 = (
int(start_time // 3600),
int(start_time % 3600 // 60),
int(start_time % 60),
)
ms1 = int((start_time - int(start_time)) * 1000)
is_last = use_gaps and idx == len(subtitles)
if is_last:
time_str = f"{h1:02d}:{m1:02d}:{s1:02d}" + (f",{ms1:03d}" if ms1 > 0 else "") + " - AUTO"
time_str = (
f"{h1:02d}:{m1:02d}:{s1:02d}"
+ (f",{ms1:03d}" if ms1 > 0 else "")
+ " - AUTO"
)
else:
h2, m2, s2 = int(end_time // 3600), int(end_time % 3600 // 60), int(end_time % 60)
h2, m2, s2 = (
int(end_time // 3600),
int(end_time % 3600 // 60),
int(end_time % 60),
)
ms2 = int((end_time - int(end_time)) * 1000)
time_str = (f"{h1:02d}:{m1:02d}:{s1:02d}" + (f",{ms1:03d}" if ms1 > 0 else "") +
" - " + f"{h2:02d}:{m2:02d}:{s2:02d}" + (f",{ms2:03d}" if ms2 > 0 else ""))
self.log_updated.emit(f"\n[{idx}/{len(subtitles)}] {time_str}: {processed_text}")
time_str = (
f"{h1:02d}:{m1:02d}:{s1:02d}"
+ (f",{ms1:03d}" if ms1 > 0 else "")
+ " - "
+ f"{h2:02d}:{m2:02d}:{s2:02d}"
+ (f",{ms2:03d}" if ms2 > 0 else "")
)
self.log_updated.emit(
f"\n[{idx}/{len(subtitles)}] {time_str}: {processed_text}"
)
# Generate TTS audio
tts_results = [r for r in tts(processed_text, voice=loaded_voice, speed=self.speed, split_pattern=None) if not self.cancel_requested]
tts_results = [
r
for r in tts(
processed_text,
voice=loaded_voice,
speed=self.speed,
split_pattern=None,
)
if not self.cancel_requested
]
audio_chunks = [r.audio for r in tts_results]
if self.cancel_requested:
if subtitle_file: subtitle_file.close()
if subtitle_file:
subtitle_file.close()
self.conversion_finished.emit("Cancelled", None)
return
# Concatenate audio and determine duration
full_audio = (
self.np.concatenate([a.numpy() if hasattr(a, "numpy") else a for a in audio_chunks])
self.np.concatenate(
[a.numpy() if hasattr(a, "numpy") else a for a in audio_chunks]
)
if audio_chunks
else self.np.zeros(int((subtitle_duration or 0) * rate), dtype="float32")
else self.np.zeros(
int((subtitle_duration or 0) * rate), dtype="float32"
)
)
audio_duration = len(full_audio) / rate
@@ -1817,38 +1946,91 @@ class ConversionThread(QThread):
end_time = start_time + audio_duration
# Speed up if needed
speedup_threshold = next_start - start_time if use_gaps else subtitle_duration
speedup_threshold = (
next_start - start_time if use_gaps else subtitle_duration
)
if audio_duration > speedup_threshold:
speed_factor = audio_duration / speedup_threshold
if getattr(self, 'subtitle_speed_method', 'tts') == "ffmpeg":
if getattr(self, "subtitle_speed_method", "tts") == "ffmpeg":
# FFmpeg time-stretch (faster processing)
self.log_updated.emit((f" -> FFmpeg time-stretch: {speed_factor:.2f}x", "grey"))
self.log_updated.emit(
(f" -> FFmpeg time-stretch: {speed_factor:.2f}x", "grey")
)
static_ffmpeg.add_paths()
num_stages = max(1, int(self.np.ceil(self.np.log(speed_factor) / self.np.log(2.0))))
num_stages = max(
1,
int(
self.np.ceil(
self.np.log(speed_factor) / self.np.log(2.0)
)
),
)
tempo = speed_factor ** (1.0 / num_stages)
filter_str = ",".join([f"atempo={tempo:.6f}"] * num_stages)
speed_proc = subprocess.Popen(
["ffmpeg", "-y", "-f", "f32le", "-ar", str(rate), "-ac", "1", "-i", "pipe:0",
"-filter:a", filter_str, "-f", "f32le", "-ar", str(rate), "-ac", "1", "pipe:1"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
[
"ffmpeg",
"-y",
"-f",
"f32le",
"-ar",
str(rate),
"-ac",
"1",
"-i",
"pipe:0",
"-filter:a",
filter_str,
"-f",
"f32le",
"-ar",
str(rate),
"-ac",
"1",
"pipe:1",
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
full_audio = self.np.frombuffer(
speed_proc.communicate(input=full_audio.tobytes())[0],
dtype="float32",
)
full_audio = self.np.frombuffer(speed_proc.communicate(input=full_audio.tobytes())[0], dtype="float32")
audio_duration = len(full_audio) / rate
else:
# TTS regeneration (better quality)
new_speed = self.speed * speed_factor
self.log_updated.emit((f" -> Regenerating at {new_speed:.2f}x speed", "grey"))
self.log_updated.emit(
(f" -> Regenerating at {new_speed:.2f}x speed", "grey")
)
tts_results = [r for r in tts(processed_text, voice=loaded_voice, speed=new_speed, split_pattern=None) if not self.cancel_requested]
tts_results = [
r
for r in tts(
processed_text,
voice=loaded_voice,
speed=new_speed,
split_pattern=None,
)
if not self.cancel_requested
]
audio_chunks = [r.audio for r in tts_results]
full_audio = (
self.np.concatenate([a.numpy() if hasattr(a, "numpy") else a for a in audio_chunks])
self.np.concatenate(
[
a.numpy() if hasattr(a, "numpy") else a
for a in audio_chunks
]
)
if audio_chunks
else self.np.zeros(int(subtitle_duration * rate), dtype="float32")
else self.np.zeros(
int(subtitle_duration * rate), dtype="float32"
)
)
audio_duration = len(full_audio) / rate
@@ -1863,7 +2045,14 @@ class ConversionThread(QThread):
# Pad or trim to subtitle duration
target_samples = int(subtitle_duration * rate)
if len(full_audio) < target_samples:
full_audio = self.np.concatenate([full_audio, self.np.zeros(target_samples - len(full_audio), dtype="float32")])
full_audio = self.np.concatenate(
[
full_audio,
self.np.zeros(
target_samples - len(full_audio), dtype="float32"
),
]
)
elif len(full_audio) > target_samples:
full_audio = full_audio[:target_samples]
@@ -1872,7 +2061,14 @@ class ConversionThread(QThread):
end_sample = start_sample + len(full_audio)
if end_sample > len(audio_buffer):
# Extend buffer if needed
audio_buffer = self.np.concatenate([audio_buffer, self.np.zeros(end_sample - len(audio_buffer), dtype="float32")])
audio_buffer = self.np.concatenate(
[
audio_buffer,
self.np.zeros(
end_sample - len(audio_buffer), dtype="float32"
),
]
)
# Mix (add) the audio - this handles overlaps by combining them
audio_buffer[start_sample:end_sample] += full_audio
@@ -1880,24 +2076,41 @@ class ConversionThread(QThread):
# Write subtitle
if subtitle_file:
if "ass" in subtitle_format:
effect = "karaoke" if self.subtitle_mode == "Sentence + Highlighting" else ""
ass_text = processed_text if replace_nl else processed_text.replace('\n', '\\N')
subtitle_file.write(f"Dialogue: 0,{self._ass_time(start_time)},{self._ass_time(end_time)},Default,,{margin},{margin},0,{effect},{alignment}{ass_text}\n")
effect = (
"karaoke"
if self.subtitle_mode == "Sentence + Highlighting"
else ""
)
ass_text = (
processed_text
if replace_nl
else processed_text.replace("\n", "\\N")
)
subtitle_file.write(
f"Dialogue: 0,{self._ass_time(start_time)},{self._ass_time(end_time)},Default,,{margin},{margin},0,{effect},{alignment}{ass_text}\n"
)
else:
subtitle_file.write(f"{srt_index}\n{self._srt_time(start_time)} --> {self._srt_time(end_time)}\n{processed_text}\n\n")
subtitle_file.write(
f"{srt_index}\n{self._srt_time(start_time)} --> {self._srt_time(end_time)}\n{processed_text}\n\n"
)
srt_index += 1
# Update progress
percent = min(int(idx / len(subtitles) * 100), 99)
elapsed = time.time() - self.etr_start_time
etr_str = ("Processing..." if elapsed <= 0.5
else f"{int(elapsed*(len(subtitles)-idx)/idx)//3600:02d}:{(int(elapsed*(len(subtitles)-idx)/idx)%3600)//60:02d}:{int(elapsed*(len(subtitles)-idx)/idx)%60:02d}")
etr_str = (
"Processing..."
if elapsed <= 0.5
else f"{int(elapsed*(len(subtitles)-idx)/idx)//3600:02d}:{(int(elapsed*(len(subtitles)-idx)/idx)%3600)//60:02d}:{int(elapsed*(len(subtitles)-idx)/idx)%60:02d}"
)
self.progress_updated.emit(percent, etr_str)
# Normalize audio buffer to prevent clipping from mixed overlaps
max_amplitude = self.np.abs(audio_buffer).max()
if max_amplitude > 1.0:
self.log_updated.emit(f"\n -> Normalizing audio (peak: {max_amplitude:.2f})")
self.log_updated.emit(
f"\n -> Normalizing audio (peak: {max_amplitude:.2f})"
)
audio_buffer = audio_buffer / max_amplitude
# Write the complete audio buffer
@@ -1910,20 +2123,25 @@ class ConversionThread(QThread):
ffmpeg_proc.stdin.close()
ffmpeg_proc.wait()
if subtitle_file: subtitle_file.close()
if subtitle_file:
subtitle_file.close()
self.progress_updated.emit(100, "00:00:00")
result_msg = (f"\nAudiobook saved to: {merged_out_path}" +
(f"\n\nSubtitle saved to: {subtitle_path}" if subtitle_path else ""))
result_msg = f"\nAudiobook saved to: {merged_out_path}" + (
f"\n\nSubtitle saved to: {subtitle_path}" if subtitle_path else ""
)
self.conversion_finished.emit((result_msg, "green"), merged_out_path)
except Exception as e:
try:
if "ffmpeg_proc" in locals() and ffmpeg_proc:
ffmpeg_proc.stdin.close(); ffmpeg_proc.terminate(); ffmpeg_proc.wait()
ffmpeg_proc.stdin.close()
ffmpeg_proc.terminate()
ffmpeg_proc.wait()
if "subtitle_file" in locals() and subtitle_file:
subtitle_file.close()
except: pass
except:
pass
self.log_updated.emit((f"Error processing subtitle file: {str(e)}", "red"))
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
@@ -2068,7 +2286,11 @@ class ConversionThread(QThread):
# Sentence-based processing with karaoke highlighting
# Use language-specific punctuation for CJK languages (without comma)
lang_punct = self.LANGUAGE_PUNCTUATION.get(self.lang_code, {})
separator = lang_punct.get('sentence', r"[.!?]") if isinstance(lang_punct, dict) else r"[.!?]"
separator = (
lang_punct.get("sentence", r"[.!?]")
if isinstance(lang_punct, dict)
else r"[.!?]"
)
current_sentence = []
word_count = 0
@@ -2131,11 +2353,19 @@ class ConversionThread(QThread):
elif self.subtitle_mode == "Sentence":
# Use language-specific punctuation for CJK languages (without comma)
lang_punct = self.LANGUAGE_PUNCTUATION.get(self.lang_code, {})
separator = lang_punct.get('sentence', r"[.!?]") if isinstance(lang_punct, dict) else r"[.!?]"
separator = (
lang_punct.get("sentence", r"[.!?]")
if isinstance(lang_punct, dict)
else r"[.!?]"
)
else: # Sentence + Comma
# Use language-specific punctuation for CJK languages (with comma)
lang_punct = self.LANGUAGE_PUNCTUATION.get(self.lang_code, {})
separator = lang_punct.get('comma', r"[.!?,]") if isinstance(lang_punct, dict) else r"[.!?,]"
separator = (
lang_punct.get("comma", r"[.!?,]")
if isinstance(lang_punct, dict)
else r"[.!?,]"
)
current_sentence = []
word_count = 0
+32 -17
View File
@@ -391,7 +391,9 @@ class InputBox(QLabel):
if hasattr(window, "subtitle_combo"):
# Only enable if language supports it
current_lang = getattr(window, "lang_code", "a")
window.subtitle_combo.setEnabled(current_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION)
window.subtitle_combo.setEnabled(
current_lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION
)
if hasattr(window, "replace_newlines_combo"):
window.replace_newlines_combo.setEnabled(True)
@@ -502,7 +504,9 @@ class InputBox(QLabel):
)
event.acceptProposedAction()
else:
self.set_error("Please drop a .txt, .epub, .pdf, .md, .srt, .ass, or .vtt file.")
self.set_error(
"Please drop a .txt, .epub, .pdf, .md, .srt, .ass, or .vtt file."
)
event.ignore()
else:
event.ignore()
@@ -1318,7 +1322,10 @@ class abogen(QWidget):
return
try:
file_path, _ = QFileDialog.getOpenFileName(
self, "Select File", "", "Supported Files (*.txt *.epub *.pdf *.md *.srt *.ass *.vtt)"
self,
"Select File",
"",
"Supported Files (*.txt *.epub *.pdf *.md *.srt *.ass *.vtt)",
)
if not file_path:
return
@@ -1934,9 +1941,7 @@ class abogen(QWidget):
self.replace_single_newlines = getattr(
queued_item, "replace_single_newlines", False
)
self.use_silent_gaps = getattr(
queued_item, "use_silent_gaps", False
)
self.use_silent_gaps = getattr(queued_item, "use_silent_gaps", False)
# Restore the original file path for save location
self.displayed_file_path = (
queued_item.save_base_path or queued_item.file_name
@@ -2083,9 +2088,7 @@ class abogen(QWidget):
self.replace_single_newlines
)
# Pass use_silent_gaps setting
self.conversion_thread.use_silent_gaps = (
self.use_silent_gaps
)
self.conversion_thread.use_silent_gaps = self.use_silent_gaps
# Pass subtitle_speed_method setting
self.conversion_thread.subtitle_speed_method = self.subtitle_speed_method
# Pass separate_chapters_format setting
@@ -2820,7 +2823,10 @@ class abogen(QWidget):
# Dialog always accepts (Yes or No), never cancels the conversion
dialog.exec()
treat_as_subtitle = dialog.use_timestamps()
if hasattr(self, "conversion_thread") and self.conversion_thread.isRunning():
if (
hasattr(self, "conversion_thread")
and self.conversion_thread.isRunning()
):
self.conversion_thread.set_timestamp_response(treat_as_subtitle)
return
@@ -2832,7 +2838,10 @@ class abogen(QWidget):
if dialog.exec() == QDialog.DialogCode.Accepted:
options = dialog.get_options()
if hasattr(self, "conversion_thread") and self.conversion_thread.isRunning():
if (
hasattr(self, "conversion_thread")
and self.conversion_thread.isRunning()
):
self.conversion_thread.set_chapter_options(options)
else:
self.cancel_conversion()
@@ -3113,12 +3122,16 @@ class abogen(QWidget):
speed_method_group = QActionGroup(self)
speed_method_group.setExclusive(True)
for method, label in [("tts", "TTS Regeneration (better quality)"),
("ffmpeg", "FFmpeg Time-stretch (better speed)")]:
for method, label in [
("tts", "TTS Regeneration (better quality)"),
("ffmpeg", "FFmpeg Time-stretch (better speed)"),
]:
action = QAction(label, speed_method_menu)
action.setCheckable(True)
action.setChecked(self.subtitle_speed_method == method)
action.triggered.connect(lambda checked, m=method: self.toggle_subtitle_speed_method(m))
action.triggered.connect(
lambda checked, m=method: self.toggle_subtitle_speed_method(m)
)
speed_method_group.addAction(action)
speed_method_menu.addAction(action)
@@ -3165,15 +3178,17 @@ class abogen(QWidget):
def toggle_use_silent_gaps(self, enabled):
# Show confirmation dialog with explanation
action = "enable" if enabled else "disable"
message = ("When enabled, allows speech to continue naturally into the silent periods between subtitles, "
message = (
"When enabled, allows speech to continue naturally into the silent periods between subtitles, "
"preventing unnecessary audio speed-up based on subtitle end timestamps.\n\nWhen disabled, ensures strict subtitle timing where "
f"audio ends exactly when the subtitle ends.\n\nDo you want to {action} this option?")
f"audio ends exactly when the subtitle ends.\n\nDo you want to {action} this option?"
)
reply = QMessageBox.question(
self,
"Use Silent Gaps Between Subtitles",
message,
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel
QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel,
)
if reply == QMessageBox.StandardButton.Ok:
+6 -1
View File
@@ -8,9 +8,14 @@ import signal
if platform.system() == "Windows":
import ctypes
from importlib.util import find_spec
try:
if (spec := find_spec("torch")) and spec.origin and os.path.exists(
if (
(spec := find_spec("torch"))
and spec.origin
and os.path.exists(
dll_path := os.path.join(os.path.dirname(spec.origin), "lib", "c10.dll")
)
):
ctypes.CDLL(os.path.normpath(dll_path))
except Exception:
+18 -7
View File
@@ -88,7 +88,10 @@ class DroppableQueueListWidget(QListWidget):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
file_path = url.toLocalFile().lower()
if url.isLocalFile() and (file_path.endswith(".txt") or file_path.endswith((".srt", ".ass", ".vtt"))):
if url.isLocalFile() and (
file_path.endswith(".txt")
or file_path.endswith((".srt", ".ass", ".vtt"))
):
self.drag_overlay.resize(self.size())
self.drag_overlay.setVisible(True)
event.acceptProposedAction()
@@ -100,7 +103,10 @@ class DroppableQueueListWidget(QListWidget):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
file_path = url.toLocalFile().lower()
if url.isLocalFile() and (file_path.endswith(".txt") or file_path.endswith((".srt", ".ass", ".vtt"))):
if url.isLocalFile() and (
file_path.endswith(".txt")
or file_path.endswith((".srt", ".ass", ".vtt"))
):
event.acceptProposedAction()
return
event.ignore()
@@ -115,7 +121,11 @@ class DroppableQueueListWidget(QListWidget):
file_paths = [
url.toLocalFile()
for url in event.mimeData().urls()
if url.isLocalFile() and (url.toLocalFile().lower().endswith(".txt") or url.toLocalFile().lower().endswith((".srt", ".ass", ".vtt")))
if url.isLocalFile()
and (
url.toLocalFile().lower().endswith(".txt")
or url.toLocalFile().lower().endswith((".srt", ".ass", ".vtt"))
)
]
if file_paths:
self.parent_dialog.add_files_from_paths(file_paths)
@@ -408,9 +418,7 @@ class QueueManager(QDialog):
parent, "replace_single_newlines", False
)
# use_silent_gaps
attrs["use_silent_gaps"] = getattr(
parent, "use_silent_gaps", False
)
attrs["use_silent_gaps"] = getattr(parent, "use_silent_gaps", False)
# subtitle_speed_method
attrs["subtitle_speed_method"] = getattr(
parent, "subtitle_speed_method", "tts"
@@ -527,7 +535,10 @@ class QueueManager(QDialog):
# Allow .txt, .srt, .ass, and .vtt files
files, _ = QFileDialog.getOpenFileNames(
self, "Select text or subtitle files", "", "Supported Files (*.txt *.srt *.ass *.vtt)"
self,
"Select text or subtitle files",
"",
"Supported Files (*.txt *.srt *.ass *.vtt)",
)
if not files:
return