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