diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff8bb2f..d9e8189 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# v1.1.1
+- Fixed adding wrong file in queue for EPUB and PDF files, ensuring the correct file is added to the queue.
+- Reformatted the code using Black.
+
# v1.1.0
- Added queue system for processing multiple items, allowing users to add multiple files and process them in a queue, mentioned by @jborza in #30 (Special thanks to @jborza for implementing this feature in PR #35)
- Added a feature that allows selecting multiple items in book handler (in right click menu) by @jborza in #31, that fixes #28
diff --git a/abogen/VERSION b/abogen/VERSION
index 1cc5f65..8cfbc90 100644
--- a/abogen/VERSION
+++ b/abogen/VERSION
@@ -1 +1 @@
-1.1.0
\ No newline at end of file
+1.1.1
\ No newline at end of file
diff --git a/abogen/book_handler.py b/abogen/book_handler.py
index e5bf8ac..5c582cb 100644
--- a/abogen/book_handler.py
+++ b/abogen/book_handler.py
@@ -849,7 +849,7 @@ class HandlerDialog(QDialog):
if iterator.value():
has_parents = True
self.treeWidget.setRootIsDecorated(has_parents)
-
+
def _update_checkbox_states(self):
"""Update the checkbox states based on the current checked chapters."""
for i in range(self.treeWidget.topLevelItemCount()):
@@ -997,10 +997,21 @@ class HandlerDialog(QDialog):
bookmark_item.setData(0, Qt.UserRole, page_id)
# only allow checking if this chapter has content
if self.content_lengths.get(page_id, 0) > 0:
- bookmark_item.setFlags(bookmark_item.flags() | Qt.ItemIsUserCheckable)
- bookmark_item.setCheckState(0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked)
+ bookmark_item.setFlags(
+ bookmark_item.flags() | Qt.ItemIsUserCheckable
+ )
+ bookmark_item.setCheckState(
+ 0,
+ (
+ Qt.Checked
+ if page_id in self.checked_chapters
+ else Qt.Unchecked
+ ),
+ )
else:
- bookmark_item.setFlags(bookmark_item.flags() & ~Qt.ItemIsUserCheckable)
+ bookmark_item.setFlags(
+ bookmark_item.flags() & ~Qt.ItemIsUserCheckable
+ )
# map for uncategorized pages
self.bookmark_items_map[page_num] = bookmark_item
@@ -1027,10 +1038,21 @@ class HandlerDialog(QDialog):
page_item.setData(0, Qt.UserRole, page_id)
# only allow checking if this sub-page has content
if self.content_lengths.get(page_id, 0) > 0:
- page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable)
- page_item.setCheckState(0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked)
+ page_item.setFlags(
+ page_item.flags() | Qt.ItemIsUserCheckable
+ )
+ page_item.setCheckState(
+ 0,
+ (
+ Qt.Checked
+ if page_id in self.checked_chapters
+ else Qt.Unchecked
+ ),
+ )
else:
- page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
+ page_item.setFlags(
+ page_item.flags() & ~Qt.ItemIsUserCheckable
+ )
added_pages.add(sub_page_num)
@@ -1038,11 +1060,15 @@ class HandlerDialog(QDialog):
covered_pages = set(added_pages)
# attach any pages without direct bookmarks under nearest preceding chapter
- uncategorized_pages = [i for i in range(len(self.pdf_doc)) if i not in covered_pages]
+ uncategorized_pages = [
+ i for i in range(len(self.pdf_doc)) if i not in covered_pages
+ ]
for page_num in uncategorized_pages:
# find nearest previous bookmark
prev_nums = [n for n in sorted(self.bookmark_items_map) if n < page_num]
- parent_item = self.bookmark_items_map[prev_nums[-1]] if prev_nums else self.treeWidget
+ parent_item = (
+ self.bookmark_items_map[prev_nums[-1]] if prev_nums else self.treeWidget
+ )
page_id = f"page_{page_num+1}"
title = f"Page {page_num+1}"
text = self.content_texts.get(page_id, "").strip()
@@ -1055,7 +1081,9 @@ class HandlerDialog(QDialog):
# only allow checking if uncategorized page has content
if self.content_lengths.get(page_id, 0) > 0:
page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable)
- page_item.setCheckState(0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked)
+ page_item.setCheckState(
+ 0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked
+ )
else:
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
@@ -1081,7 +1109,9 @@ class HandlerDialog(QDialog):
# only allow checking if standalone page has content
if self.content_lengths.get(page_id, 0) > 0:
page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable)
- page_item.setCheckState(0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked)
+ page_item.setCheckState(
+ 0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked
+ )
else:
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
@@ -1182,7 +1212,7 @@ class HandlerDialog(QDialog):
self.save_chapters_checkbox = QCheckBox(checkbox_text, self)
self.save_chapters_checkbox.setChecked(self.save_chapters_separately)
self.save_chapters_checkbox.stateChanged.connect(self.on_save_chapters_changed)
- leftLayout.addWidget(self.save_chapters_checkbox)
+ leftLayout.addWidget(self.save_chapters_checkbox)
self.merge_chapters_checkbox = QCheckBox(
"Create a merged version at the end", self
)
@@ -1191,7 +1221,7 @@ class HandlerDialog(QDialog):
self.on_merge_chapters_changed
)
leftLayout.addWidget(self.merge_chapters_checkbox)
-
+
self.save_as_project_checkbox = QCheckBox(
"Save in a project folder with metadata", self
)
@@ -1501,7 +1531,9 @@ class HandlerDialog(QDialog):
authors_text = ", ".join(self.book_metadata["authors"])
html_content += f"
By {authors_text}
"
- if self.book_metadata["publisher"] or self.book_metadata.get("publication_year"):
+ if self.book_metadata["publisher"] or self.book_metadata.get(
+ "publication_year"
+ ):
pub_info = []
if self.book_metadata["publisher"]:
pub_info.append(f"Published by {self.book_metadata['publisher']}")
@@ -1543,7 +1575,9 @@ class HandlerDialog(QDialog):
try:
author_items = self.book.get_metadata("DC", "creator")
if author_items:
- metadata["authors"] = [author[0] for author in author_items if len(author) > 0]
+ metadata["authors"] = [
+ author[0] for author in author_items if len(author) > 0
+ ]
except Exception as e:
logging.warning(f"Error extracting author metadata: {e}")
@@ -1560,14 +1594,14 @@ class HandlerDialog(QDialog):
metadata["publisher"] = publisher_items[0][0]
except Exception as e:
logging.warning(f"Error extracting publisher metadata: {e}")
-
+
# Try to extract publication year
try:
date_items = self.book.get_metadata("DC", "date")
if date_items and len(date_items) > 0:
date_str = date_items[0][0]
# Try to extract just the year from the date string
- year_match = re.search(r'\b(19|20)\d{2}\b', date_str)
+ year_match = re.search(r"\b(19|20)\d{2}\b", date_str)
if year_match:
metadata["publication_year"] = year_match.group(0)
else:
@@ -1603,16 +1637,16 @@ class HandlerDialog(QDialog):
metadata["description"] = f"Keywords: {keywords}"
metadata["publisher"] = pdf_info.get("creator", None)
-
+
# Try to extract publication date from PDF metadata
if "creationDate" in pdf_info:
date_str = pdf_info["creationDate"]
- year_match = re.search(r'D:(\d{4})', date_str)
+ year_match = re.search(r"D:(\d{4})", date_str)
if year_match:
metadata["publication_year"] = year_match.group(1)
elif "modDate" in pdf_info:
date_str = pdf_info["modDate"]
- year_match = re.search(r'D:(\d{4})', date_str)
+ year_match = re.search(r"D:(\d{4})", date_str)
if year_match:
metadata["publication_year"] = year_match.group(1)
@@ -1634,22 +1668,26 @@ class HandlerDialog(QDialog):
def _format_metadata_tags(self):
"""Format metadata tags for insertion at the beginning of the text"""
import datetime
-
+
metadata = self.book_metadata
filename = os.path.splitext(os.path.basename(self.book_path))[0]
current_year = str(datetime.datetime.now().year)
-
+
# Get values with fallbacks
title = metadata.get("title") or filename
authors = metadata.get("authors") or ["Unknown"]
authors_text = ", ".join(authors)
album_artist = authors_text or "Unknown"
- year = metadata.get("publication_year") or current_year # Use publication year if available
-
+ year = (
+ metadata.get("publication_year") or current_year
+ ) # Use publication year if available
+
# Count chapters/pages
total_chapters = len(self.checked_chapters)
- chapter_text = f"{total_chapters} {'Chapters' if self.file_type == 'epub' else 'Pages'}"
-
+ chapter_text = (
+ f"{total_chapters} {'Chapters' if self.file_type == 'epub' else 'Pages'}"
+ )
+
# Format metadata tags
metadata_tags = [
f"<>",
@@ -1658,9 +1696,9 @@ class HandlerDialog(QDialog):
f"<>",
f"<>",
f"<>",
- f"<>"
+ f"<>",
]
-
+
return "\n".join(metadata_tags)
def _get_epub_selected_text(self):
@@ -1669,7 +1707,7 @@ class HandlerDialog(QDialog):
# Add metadata tags at the beginning
metadata_tags = self._format_metadata_tags()
-
+
item_order_counter = 0
ordered_checked_items = []
@@ -1730,7 +1768,10 @@ class HandlerDialog(QDialog):
if text:
all_content.append(text)
included_text_ids.add(page_id)
- return metadata_tags + "\n\n" + "\n\n".join(all_content), all_checked_identifiers
+ return (
+ metadata_tags + "\n\n" + "\n\n".join(all_content),
+ all_checked_identifiers,
+ )
iterator = QTreeWidgetItemIterator(self.treeWidget)
while iterator.value():
@@ -1788,7 +1829,10 @@ class HandlerDialog(QDialog):
included_text_ids.add(identifier)
iterator += 1
- return metadata_tags + "\n\n" + "\n\n".join([t[1] for t in section_titles]), all_checked_identifiers
+ return (
+ metadata_tags + "\n\n" + "\n\n".join([t[1] for t in section_titles]),
+ all_checked_identifiers,
+ )
def on_save_chapters_changed(self, state):
self.save_chapters_separately = bool(state)
@@ -1815,21 +1859,21 @@ class HandlerDialog(QDialog):
def get_save_as_project(self):
return self.save_as_project
-
+
def check_selected_items(self):
self.set_selected_items_checked(True)
-
+
def uncheck_selected_items(self):
self.set_selected_items_checked(False)
-
+
def set_selected_items_checked(self, state: bool):
print(f"Checking selected items: {state}")
self.treeWidget.blockSignals(True)
for item in self.treeWidget.selectedItems():
- if item.flags() & Qt.ItemIsUserCheckable:
- item.setCheckState(0, Qt.Checked if state else Qt.Unchecked)
+ if item.flags() & Qt.ItemIsUserCheckable:
+ item.setCheckState(0, Qt.Checked if state else Qt.Unchecked)
self.treeWidget.blockSignals(False)
- self._update_checked_set_from_tree()
+ self._update_checked_set_from_tree()
def on_tree_context_menu(self, pos):
item = self.treeWidget.itemAt(pos)
@@ -1842,7 +1886,7 @@ class HandlerDialog(QDialog):
action.triggered.connect(self.uncheck_selected_items)
menu.exec_(self.treeWidget.mapToGlobal(pos))
return
-
+
if (
not item
or item.childCount() == 0
diff --git a/abogen/constants.py b/abogen/constants.py
index a403302..b7074e1 100644
--- a/abogen/constants.py
+++ b/abogen/constants.py
@@ -129,4 +129,4 @@ COLORS = {
"DARK_DISABLED": "#535353",
"LIGHT_BG": "#eff0f1",
"LIGHT_DISABLED": "#9a9999",
-}
\ No newline at end of file
+}
diff --git a/abogen/conversion.py b/abogen/conversion.py
index 0a31e60..dfc62eb 100644
--- a/abogen/conversion.py
+++ b/abogen/conversion.py
@@ -10,13 +10,20 @@ from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
import soundfile as sf
from abogen.utils import clean_text, create_process
-from abogen.constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS, COLORS, CHAPTER_OPTIONS_COUNTDOWN
+from abogen.constants import (
+ PROGRAM_NAME,
+ LANGUAGE_DESCRIPTIONS,
+ SAMPLE_VOICE_TEXTS,
+ COLORS,
+ CHAPTER_OPTIONS_COUNTDOWN,
+)
from abogen.voice_formulas import get_new_voice
import abogen.hf_tracker as hf_tracker
import static_ffmpeg
import threading # for efficient waiting
import subprocess
+
def get_sample_voice_text(lang_code):
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
@@ -73,7 +80,9 @@ class ChapterOptionsDialog(QDialog):
# Countdown label
self.countdown_seconds = CHAPTER_OPTIONS_COUNTDOWN
- 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)
@@ -96,7 +105,9 @@ class ChapterOptionsDialog(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() # Simulate OK click
@@ -152,7 +163,7 @@ class ConversionThread(QThread):
start_time,
total_char_count,
use_gpu=True,
- from_queue=False
+ from_queue=False,
): # Add use_gpu parameter
super().__init__()
self._chapter_options_event = threading.Event()
@@ -182,15 +193,17 @@ class ConversionThread(QThread):
self.use_gpu = use_gpu # Store the GPU setting
self.max_subtitle_words = 50 # Default value, will be overridden from GUI
- def _stream_audio_in_chunks(self, segments, process_func, progress_prefix="Processing"):
+ def _stream_audio_in_chunks(
+ self, segments, process_func, progress_prefix="Processing"
+ ):
"""
Process audio segments in memory-efficient chunks
-
+
Args:
segments: List of audio segments to process
process_func: Function that takes (segment_bytes, is_last) and processes a chunk
progress_prefix: Prefix for progress messages
-
+
Returns:
Total samples processed
"""
@@ -199,83 +212,98 @@ class ConversionThread(QThread):
samples_processed = 0
self.log_updated.emit(f"\n{progress_prefix} segments...")
-
+
# Stream each segment individually
for i, segment in enumerate(segments):
try:
# Handle both NumPy arrays and PyTorch tensors
- if hasattr(segment, 'astype'):
+ if hasattr(segment, "astype"):
segment_bytes = segment.astype("float32").tobytes()
else:
segment_bytes = segment.cpu().numpy().astype("float32").tobytes()
- is_last = (i == len(segments) - 1)
-
+ is_last = i == len(segments) - 1
+
# Update progress periodically - skip if there's only one segment
if (i % 20 == 0 or is_last) and len(segments) > 1:
progress_percent = int((samples_processed / total_samples) * 100)
- self.log_updated.emit(f"{progress_prefix} segment {i+1}/{len(segments)} ({progress_percent}% complete)")
-
+ self.log_updated.emit(
+ f"{progress_prefix} segment {i+1}/{len(segments)} ({progress_percent}% complete)"
+ )
+
# Process this segment
process_func(segment_bytes, is_last)
-
+
# Update samples processed
samples_processed += len(segment)
-
+
# Clear segment bytes from memory
del segment_bytes
except Exception as e:
self.log_updated.emit(f"Error processing segment {i}: {str(e)}")
raise
-
+
return samples_processed
- def _process_audio_segments(self, audio_segments, output_path, output_format, use_ffmpeg=False, ffmpeg_args=None):
+ def _process_audio_segments(
+ self,
+ audio_segments,
+ output_path,
+ output_format,
+ use_ffmpeg=False,
+ ffmpeg_args=None,
+ ):
"""
Process audio segments to a target format with memory-efficient handling
-
+
Args:
audio_segments: List of audio segments to process
output_path: Path for output file
output_format: Format of output (wav, mp3, flac, opus, m4b)
use_ffmpeg: Whether to use FFmpeg instead of soundfile
ffmpeg_args: Optional additional FFmpeg arguments when use_ffmpeg=True
-
+
Returns:
Tuple of (success, output_path)
"""
self.log_updated.emit(f"\nProcessing audio data to {output_format.upper()}...")
segments_count = len(audio_segments)
-
+
# Handle direct streaming for WAV (the only format supporting append mode)
if output_format == "wav" and not use_ffmpeg:
try:
# Write first segment
sf.write(output_path, audio_segments[0], 24000, format="wav")
-
+
# Append remaining segments
for i, segment in enumerate(audio_segments[1:], 1):
- with sf.SoundFile(output_path, mode='r+') as f:
+ with sf.SoundFile(output_path, mode="r+") as f:
f.seek(0, sf.SEEK_END)
f.write(segment)
return True, output_path
except Exception as e:
self.log_updated.emit((f"Error writing WAV file: {str(e)}", "red"))
return False, None
-
+
# For formats requiring FFmpeg (opus, m4b) or when explicitly requested
if use_ffmpeg or output_format in ["opus", "m4b"]:
static_ffmpeg.add_paths()
-
+
# Basic FFmpeg command
cmd = [
- "ffmpeg", "-y",
- "-thread_queue_size", "32768",
- "-f", "f32le",
- "-ar", "24000",
- "-ac", "1",
- "-i", "pipe:0"
+ "ffmpeg",
+ "-y",
+ "-thread_queue_size",
+ "32768",
+ "-f",
+ "f32le",
+ "-ar",
+ "24000",
+ "-ac",
+ "1",
+ "-i",
+ "pipe:0",
]
-
+
# Add custom FFmpeg arguments if provided
if ffmpeg_args:
cmd.extend(ffmpeg_args)
@@ -289,48 +317,62 @@ class ConversionThread(QThread):
cmd.extend(["-c:a", "flac", "-compression_level", "8"])
else:
cmd.extend(["-c:a", "aac", "-q:a", "2"])
-
+
# Add output path
cmd.append(output_path)
-
+
# Create process
proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
-
+
# Process segments
try:
# Use the unified streaming function
def process_chunk(chunk_bytes, is_last):
proc.stdin.write(chunk_bytes)
-
- self._stream_audio_in_chunks(audio_segments, process_chunk,
- progress_prefix=f"Processing {output_format.upper()}")
-
+
+ self._stream_audio_in_chunks(
+ audio_segments,
+ process_chunk,
+ progress_prefix=f"Processing {output_format.upper()}",
+ )
+
# Close stdin and wait for process to complete
proc.stdin.close()
if proc.wait() != 0:
- self.log_updated.emit((f"{output_format.upper()} conversion failed.", "red"))
+ self.log_updated.emit(
+ (f"{output_format.upper()} conversion failed.", "red")
+ )
return False, None
-
+
return True, output_path
-
+
except Exception as e:
- self.log_updated.emit((f"Error during {output_format.upper()} conversion: {str(e)}", "red"))
+ self.log_updated.emit(
+ (
+ f"Error during {output_format.upper()} conversion: {str(e)}",
+ "red",
+ )
+ )
proc.stdin.close()
try:
proc.terminate()
except:
pass
return False, None
-
+
# For formats supported by soundfile (mp3, flac)
else:
try:
- with sf.SoundFile(output_path, 'w', samplerate=24000, channels=1, format=output_format) as f:
+ with sf.SoundFile(
+ output_path, "w", samplerate=24000, channels=1, format=output_format
+ ) as f:
for i, segment in enumerate(audio_segments):
f.write(segment)
return True, output_path
except Exception as e:
- self.log_updated.emit((f"Error processing {output_format.upper()} file: {str(e)}", "red"))
+ self.log_updated.emit(
+ (f"Error processing {output_format.upper()} file: {str(e)}", "red")
+ )
return False, None
def run(self):
@@ -341,12 +383,14 @@ class ConversionThread(QThread):
hf_tracker.set_log_callback(lambda msg: self.log_updated.emit(msg))
# Show configuration
self.log_updated.emit("Configuration:")
-
+
# Use file_name for logs if from_queue, otherwise use display_path if available
if getattr(self, "from_queue", False):
display_file = self.file_name
else:
- display_file = self.display_path if self.display_path else self.file_name
+ display_file = (
+ self.display_path if self.display_path else self.file_name
+ )
self.log_updated.emit(f"- Input File: {display_file}")
@@ -363,7 +407,9 @@ class ConversionThread(QThread):
self.log_updated.emit(f"- Speed: {self.speed}")
self.log_updated.emit(f"- Subtitle mode: {self.subtitle_mode}")
self.log_updated.emit(f"- Output format: {self.output_format}")
- self.log_updated.emit(f"- Subtitle format: {getattr(self, 'subtitle_format', 'srt')}")
+ self.log_updated.emit(
+ f"- Subtitle format: {getattr(self, 'subtitle_format', 'srt')}"
+ )
self.log_updated.emit(f"- Save option: {self.save_option}")
if self.replace_single_newlines:
self.log_updated.emit(f"- Replace single newlines: Yes")
@@ -382,8 +428,10 @@ class ConversionThread(QThread):
f"- Merge chapters at the end: {'Yes' if merge_at_end else 'No'}"
)
# Display the separate chapters format if it's set
- separate_format = getattr(self, 'separate_chapters_format', 'wav')
- self.log_updated.emit(f"- Separate chapters format: {separate_format}")
+ separate_format = getattr(self, "separate_chapters_format", "wav")
+ self.log_updated.emit(
+ f"- Separate chapters format: {separate_format}"
+ )
if self.save_option == "Choose output folder":
self.log_updated.emit(
@@ -408,7 +456,7 @@ class ConversionThread(QThread):
# Clean up text using utility function
text = clean_text(text)
-
+
# Remove metadata markers from the text to be processed
metadata_pattern = r"<]*>>"
text = re.sub(metadata_pattern, "", text)
@@ -478,7 +526,6 @@ class ConversionThread(QThread):
chapters_out_dir = None
suffix = ""
-
# Use file_name for logs if from_queue, otherwise use display_path if available
if getattr(self, "from_queue", False):
base_path = self.file_name
@@ -701,42 +748,61 @@ class ConversionThread(QThread):
# Find last word boundary before limit
pos = sanitized[:MAX_LEN].rfind("_")
# Use word boundary if found, otherwise use hard limit
- sanitized = sanitized[:pos if pos > 0 else MAX_LEN].rstrip("_")
+ sanitized = sanitized[: pos if pos > 0 else MAX_LEN].rstrip("_")
chapter_filename = f"{chapter_idx:02d}_{sanitized}"
# Use separate_chapters_format
- separate_format = getattr(self, 'separate_chapters_format', 'wav')
-
+ separate_format = getattr(self, "separate_chapters_format", "wav")
+
chapter_out_path = os.path.join(
chapters_out_dir, f"{chapter_filename}.{separate_format}"
)
# Process audio segments using the unified function
success, chapter_out_path = self._process_audio_segments(
- chapter_audio_segments, chapter_out_path, separate_format, use_ffmpeg=(separate_format in ["opus", "m4b"])
+ chapter_audio_segments,
+ chapter_out_path,
+ separate_format,
+ use_ffmpeg=(separate_format in ["opus", "m4b"]),
)
-
+
if not success:
- self.log_updated.emit((f"Failed to write {separate_format.upper()} file.", "red"))
+ self.log_updated.emit(
+ (f"Failed to write {separate_format.upper()} file.", "red")
+ )
continue
# Generate subtitle file for chapter if not Disabled
if self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
- subtitle_format = getattr(self, 'subtitle_format', 'srt')
- file_extension = 'ass' if 'ass' in subtitle_format else 'srt'
+ subtitle_format = getattr(self, "subtitle_format", "srt")
+ file_extension = "ass" if "ass" in subtitle_format else "srt"
chapter_subtitle_path = os.path.join(
chapters_out_dir, f"{chapter_filename}.{file_extension}"
)
-
- if 'ass' in subtitle_format:
+
+ if "ass" in subtitle_format:
# Generate ASS subtitle
- is_centered = subtitle_format in ("ass_centered_wide", "ass_centered_narrow")
- is_narrow = subtitle_format in ("ass_narrow", "ass_centered_narrow")
- self._write_ass_subtitle(chapter_subtitle_path, chapter_subtitle_entries, is_centered, is_narrow)
+ is_centered = subtitle_format in (
+ "ass_centered_wide",
+ "ass_centered_narrow",
+ )
+ is_narrow = subtitle_format in (
+ "ass_narrow",
+ "ass_centered_narrow",
+ )
+ self._write_ass_subtitle(
+ chapter_subtitle_path,
+ chapter_subtitle_entries,
+ is_centered,
+ is_narrow,
+ )
else:
# Generate SRT subtitle (default)
with open(
- chapter_subtitle_path, "w", encoding="utf-8", errors="replace"
+ chapter_subtitle_path,
+ "w",
+ encoding="utf-8",
+ errors="replace",
) as srt_file:
for i, (start, end, text) in enumerate(
chapter_subtitle_entries, 1
@@ -768,8 +834,8 @@ class ConversionThread(QThread):
or not self.save_chapters_separately
or getattr(self, "merge_chapters_at_end", True)
)
-
- intended_output_format = self.output_format # Store the original choice
+
+ intended_output_format = self.output_format # Store the original choice
if audio_segments and merge_chapters:
self.log_updated.emit("\nFinalizing audio file...")
@@ -778,7 +844,7 @@ class ConversionThread(QThread):
base_filepath_no_ext = os.path.join(out_dir, f"{base_name}{suffix}")
final_out_path = None
-
+
# Use dedicated chapter processing for M4B when we have chapters
if intended_output_format == "m4b":
self.log_updated.emit("\nGenerating audio with chapters...")
@@ -788,35 +854,52 @@ class ConversionThread(QThread):
else:
# Process audio segments using the unified function
success, final_out_path = self._process_audio_segments(
- audio_segments, f"{base_filepath_no_ext}.{intended_output_format}",
- intended_output_format,
- use_ffmpeg=(intended_output_format in ["opus", "m4b"])
+ audio_segments,
+ f"{base_filepath_no_ext}.{intended_output_format}",
+ intended_output_format,
+ use_ffmpeg=(intended_output_format in ["opus", "m4b"]),
)
-
+
if not success:
final_out_path = None
-
+
if not final_out_path:
self.log_updated.emit(("Audio generation failed.", "red"))
- self.conversion_finished.emit(("Audio generation failed.", "red"), None)
+ self.conversion_finished.emit(
+ ("Audio generation failed.", "red"), None
+ )
return
# Subtitle and final message logic
if final_out_path:
if self.subtitle_mode != "Disabled":
- subtitle_format = getattr(self, 'subtitle_format', 'srt')
- file_extension = 'ass' if 'ass' in subtitle_format else 'srt'
- subtitle_path = os.path.splitext(final_out_path)[0] + f".{file_extension}"
-
- if 'ass' in subtitle_format:
+ subtitle_format = getattr(self, "subtitle_format", "srt")
+ file_extension = "ass" if "ass" in subtitle_format else "srt"
+ subtitle_path = (
+ os.path.splitext(final_out_path)[0] + f".{file_extension}"
+ )
+
+ if "ass" in subtitle_format:
# Generate ASS subtitle
- is_centered = subtitle_format in ("ass_centered_wide", "ass_centered_narrow")
- is_narrow = subtitle_format in ("ass_narrow", "ass_centered_narrow")
- self._write_ass_subtitle(subtitle_path, subtitle_entries, is_centered, is_narrow)
+ is_centered = subtitle_format in (
+ "ass_centered_wide",
+ "ass_centered_narrow",
+ )
+ is_narrow = subtitle_format in (
+ "ass_narrow",
+ "ass_centered_narrow",
+ )
+ self._write_ass_subtitle(
+ subtitle_path, subtitle_entries, is_centered, is_narrow
+ )
else:
# Generate SRT subtitle (default)
- with open(subtitle_path, "w", encoding="utf-8", errors="replace") as srt_file:
- for i, (start, end, text) in enumerate(subtitle_entries, 1):
+ with open(
+ subtitle_path, "w", encoding="utf-8", errors="replace"
+ ) as srt_file:
+ for i, (start, end, text) in enumerate(
+ subtitle_entries, 1
+ ):
srt_file.write(
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
)
@@ -829,11 +912,16 @@ class ConversionThread(QThread):
)
else:
self.conversion_finished.emit(
- (f"\nAudiobook saved to: {final_out_path}", "green"), final_out_path
+ (f"\nAudiobook saved to: {final_out_path}", "green"),
+ final_out_path,
)
else:
- self.log_updated.emit(("Audio generation failed (final_out_path was not set).", "red"))
- self.conversion_finished.emit(("Audio generation failed.", "red"), None)
+ self.log_updated.emit(
+ ("Audio generation failed (final_out_path was not set).", "red")
+ )
+ self.conversion_finished.emit(
+ ("Audio generation failed.", "red"), None
+ )
elif audio_segments and not merge_chapters:
self.conversion_finished.emit(
(
@@ -856,12 +944,14 @@ class ConversionThread(QThread):
self.waiting_for_user_input = False
self._chapter_options_event.set()
- def _generate_m4b_with_chapters(self, audio_segments, chapters_time, base_filepath_no_ext):
+ def _generate_m4b_with_chapters(
+ self, audio_segments, chapters_time, base_filepath_no_ext
+ ):
"""Generate M4B file with chapters from audio segments"""
final_wav_path = f"{base_filepath_no_ext}.wav"
output_m4b_path = f"{base_filepath_no_ext}.m4b"
chapters_info_path = f"{base_filepath_no_ext}_chapters.txt"
-
+
# Early check for single/no chapter case
if not chapters_time or len(chapters_time) <= 1:
self.log_updated.emit(
@@ -877,7 +967,9 @@ class ConversionThread(QThread):
if success:
return wav_path
else:
- self.log_updated.emit((f"\nFailed to save single/no chapter audio as WAV", "red"))
+ self.log_updated.emit(
+ (f"\nFailed to save single/no chapter audio as WAV", "red")
+ )
return None
try:
@@ -885,46 +977,61 @@ class ConversionThread(QThread):
with open(chapters_info_path, "w", encoding="utf-8") as f:
f.write(";FFMETADATA1\n")
for chapter in chapters_time:
- chapter_title = chapter['chapter'].replace('=', '\\=')
+ chapter_title = chapter["chapter"].replace("=", "\\=")
f.write(f"[CHAPTER]\n")
f.write(f"TIMEBASE=1/1000\n")
f.write(f"START={int(chapter['start']*1000)}\n")
f.write(f"END={int(chapter['end']*1000)}\n")
f.write(f"title={chapter_title}\n\n")
-
+
# For M4B with chapters, we need to use input file for chapter information
static_ffmpeg.add_paths()
metadata_options = self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
-
+
# Use pipe-based approach for audio input with special args for M4B chapters
ffmpeg_args = [
- "-i", chapters_info_path,
- "-map", "0:a",
- "-map_metadata", "1",
- "-map_chapters", "1",
+ "-i",
+ chapters_info_path,
+ "-map",
+ "0:a",
+ "-map_metadata",
+ "1",
+ "-map_chapters",
+ "1",
*metadata_options,
- "-c:a", "aac",
- "-q:a", "2", # Quality-based VBR for better quality control
- "-movflags", "+faststart+use_metadata_tags", # Added for better compatibility
+ "-c:a",
+ "aac",
+ "-q:a",
+ "2", # Quality-based VBR for better quality control
+ "-movflags",
+ "+faststart+use_metadata_tags", # Added for better compatibility
]
# Use the established unified method with M4B-specific args
success, out_path = self._process_audio_segments(
- audio_segments, output_m4b_path, "m4b", use_ffmpeg=True, ffmpeg_args=ffmpeg_args
+ audio_segments,
+ output_m4b_path,
+ "m4b",
+ use_ffmpeg=True,
+ ffmpeg_args=ffmpeg_args,
)
-
+
# Clean up the temporary chapter metadata file
if os.path.exists(chapters_info_path):
try:
os.remove(chapters_info_path)
except Exception as e:
- self.log_updated.emit((f"Warning: Could not delete chapters file: {e}", "orange"))
+ self.log_updated.emit(
+ (f"Warning: Could not delete chapters file: {e}", "orange")
+ )
if success:
return out_path
-
+
# If M4B generation failed, fallback to WAV
- self.log_updated.emit((f"M4B conversion failed. Falling back to WAV.\n", "red"))
+ self.log_updated.emit(
+ (f"M4B conversion failed. Falling back to WAV.\n", "red")
+ )
success, wav_path = self._process_audio_segments(
audio_segments, final_wav_path, "wav", use_ffmpeg=False
)
@@ -936,30 +1043,37 @@ class ConversionThread(QThread):
except Exception as e:
# General error during M4B generation - create WAV file directly as final output
- self.log_updated.emit((f"Error during M4B generation: {str(e)}.\n\nFalling back to WAV.\n", "red"))
-
+ self.log_updated.emit(
+ (
+ f"Error during M4B generation: {str(e)}.\n\nFalling back to WAV.\n",
+ "red",
+ )
+ )
+
# Use the established unified method to create a WAV file as fallback
success, wav_path = self._process_audio_segments(
audio_segments, final_wav_path, "wav", use_ffmpeg=False
)
-
+
# Clean up temp files
if os.path.exists(chapters_info_path):
try:
os.remove(chapters_info_path)
except Exception:
pass
-
+
if success:
return wav_path
else:
- self.log_updated.emit((f"Critical error: Failed to save WAV fallback", "red"))
+ self.log_updated.emit(
+ (f"Critical error: Failed to save WAV fallback", "red")
+ )
return None
def _extract_and_add_metadata_tags_to_ffmpeg_cmd(self):
"""Extract metadata tags from text content and add them to ffmpeg command"""
metadata_options = []
-
+
# Get the input text (either direct or from file)
text = ""
if self.is_direct_text:
@@ -967,64 +1081,77 @@ class ConversionThread(QThread):
else:
try:
encoding = detect_encoding(self.file_name)
- with open(self.file_name, "r", encoding=encoding, errors="replace") as file:
+ with open(
+ self.file_name, "r", encoding=encoding, errors="replace"
+ ) as file:
text = file.read()
except Exception as e:
- self.log_updated.emit(f"Warning: Could not read file for metadata extraction: {e}")
+ self.log_updated.emit(
+ f"Warning: Could not read file for metadata extraction: {e}"
+ )
return []
-
+
# Extract metadata tags using regex
title_match = re.search(r"<]*)>>", text)
- artist_match = re.search(r"<]*)>>", text)
+ artist_match = re.search(r"<]*)>>", text)
album_match = re.search(r"<]*)>>", text)
year_match = re.search(r"<]*)>>", text)
album_artist_match = re.search(r"<]*)>>", text)
composer_match = re.search(r"<]*)>>", text)
genre_match = re.search(r"<]*)>>", text)
-
+
# Use display path or filename as fallback for title
-
+
# Use file_name for logs if from_queue, otherwise use display_path if available
if getattr(self, "from_queue", False):
filename = os.path.splitext(os.path.basename(self.file_name))[0]
else:
- filename = os.path.splitext(os.path.basename(self.display_path if self.display_path else self.file_name))[0]
+ filename = os.path.splitext(
+ os.path.basename(
+ self.display_path if self.display_path else self.file_name
+ )
+ )[0]
if title_match:
metadata_options.extend(["-metadata", f"title={title_match.group(1)}"])
else:
metadata_options.extend(["-metadata", f"title={filename}"])
-
+
# Add artist metadata
if artist_match:
metadata_options.extend(["-metadata", f"artist={artist_match.group(1)}"])
else:
metadata_options.extend(["-metadata", f"artist=Unknown"])
-
+
# Add album metadata
if album_match:
metadata_options.extend(["-metadata", f"album={album_match.group(1)}"])
else:
metadata_options.extend(["-metadata", f"album={filename}"])
-
+
# Add year metadata
if year_match:
metadata_options.extend(["-metadata", f"date={year_match.group(1)}"])
else:
# Use current year if year is not specified
import datetime
+
current_year = datetime.datetime.now().year
metadata_options.extend(["-metadata", f"date={current_year}"])
# Add album artist metadata
if album_artist_match:
- metadata_options.extend(["-metadata", f"album_artist={album_artist_match.group(1)}"])
+ metadata_options.extend(
+ ["-metadata", f"album_artist={album_artist_match.group(1)}"]
+ )
else:
metadata_options.extend(["-metadata", f"album_artist=Unknown"])
# Add composer metadata
if composer_match:
- metadata_options.extend(["-metadata", f"composer={composer_match.group(1)}"])
+ metadata_options.extend(
+ ["-metadata", f"composer={composer_match.group(1)}"]
+ )
else:
metadata_options.extend(["-metadata", f"composer=Narrator"])
@@ -1033,7 +1160,7 @@ class ConversionThread(QThread):
metadata_options.extend(["-metadata", f"genre={genre_match.group(1)}"])
else:
metadata_options.extend(["-metadata", f"genre=Audiobook"])
-
+
# Add these to ffmpeg command
return metadata_options
@@ -1044,7 +1171,7 @@ class ConversionThread(QThread):
s = int(t % 60)
ms = int((t - int(t)) * 1000)
return f"{h:02}:{m:02}:{s:02},{ms:03}"
-
+
def _ass_time(self, t):
"""Helper function to format time for ASS files"""
h = int(t // 3600)
@@ -1139,17 +1266,21 @@ class ConversionThread(QThread):
(group[0]["start"], group[-1]["end"], text.strip())
)
- def _write_ass_subtitle(self, file_path, subtitle_entries, is_centered=False, is_narrow=False):
+ def _write_ass_subtitle(
+ self, file_path, subtitle_entries, is_centered=False, is_narrow=False
+ ):
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
# Minimal ASS header
f.write("[Script Info]\n")
f.write("Title: Generated by Abogen\n")
f.write("ScriptType: v4.00+\n\n")
-
+
# Only events section, use override tags for positioning
f.write("[Events]\n")
- f.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
-
+ f.write(
+ "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
+ )
+
# Set margin based on is_narrow parameter
margin = "90" if is_narrow else ""
alignment_tag = ""
@@ -1161,7 +1292,9 @@ class ConversionThread(QThread):
for i, (start, end, text) in enumerate(subtitle_entries, 1):
start_time = self._ass_time(start)
end_time = self._ass_time(end)
- f.write(f"Dialogue: 0,{start_time},{end_time},Default,,{margin},{margin},0,,{alignment_tag}{text}\n")
+ f.write(
+ f"Dialogue: 0,{start_time},{end_time},Default,,{margin},{margin},0,,{alignment_tag}{text}\n"
+ )
def cancel(self):
self.cancel_requested = True
@@ -1196,11 +1329,13 @@ class VoicePreviewThread(QThread):
self.voice = voice
self.speed = speed
self.use_gpu = use_gpu
-
+
# Cache location for preview audio
- self.cache_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME, "preview_cache")
+ self.cache_dir = os.path.join(
+ tempfile.gettempdir(), PROGRAM_NAME, "preview_cache"
+ )
os.makedirs(self.cache_dir, exist_ok=True)
-
+
# Calculate cache path
self.cache_path = self._get_cache_path()
@@ -1208,10 +1343,12 @@ class VoicePreviewThread(QThread):
"""Generate a unique filename for the voice with its parameters"""
# For a voice formula, use a hash of the formula
if "*" in self.voice:
- voice_id = f"voice_formula_{hashlib.md5(self.voice.encode()).hexdigest()[:8]}"
+ voice_id = (
+ f"voice_formula_{hashlib.md5(self.voice.encode()).hexdigest()[:8]}"
+ )
else:
voice_id = self.voice
-
+
# Create a unique filename based on voice_id, language, and speed
filename = f"{voice_id}_{self.lang_code}_{self.speed:.2f}.wav"
return os.path.join(self.cache_dir, filename)
@@ -1220,7 +1357,7 @@ class VoicePreviewThread(QThread):
print(
f"\nVoice: {self.voice}\nLanguage: {self.lang_code}\nSpeed: {self.speed}\nGPU: {self.use_gpu}\n"
)
-
+
# Generate the preview and save to cache
try:
device = "cuda" if self.use_gpu else "cpu"
@@ -1268,7 +1405,7 @@ class PlayAudioThread(QThread):
# Wait until playback is finished or canceled
while pygame.mixer.music.get_busy() and not self.is_canceled:
_time.sleep(0.2)
-
+
# Make sure to clean up regardless of how we exited the loop
try:
pygame.mixer.music.stop()
@@ -1277,21 +1414,24 @@ class PlayAudioThread(QThread):
except Exception:
# Ignore any errors during cleanup
pass
-
+
self.finished.emit()
except Exception as e:
# Handle initialization errors separately to give better error messages
if "mixer not initialized" in str(e):
- self.error.emit("Audio playback error: The audio system was not properly initialized")
+ self.error.emit(
+ "Audio playback error: The audio system was not properly initialized"
+ )
else:
self.error.emit(f"Audio playback error: {str(e)}")
-
+
def stop(self):
"""Safely stop playback"""
self.is_canceled = True
# Try to stop pygame if it's running, but catch all exceptions
try:
import pygame
+
if pygame.mixer.get_init():
if pygame.mixer.music.get_busy():
pygame.mixer.music.stop()
diff --git a/abogen/gui.py b/abogen/gui.py
index ff22794..066ac44 100644
--- a/abogen/gui.py
+++ b/abogen/gui.py
@@ -74,7 +74,7 @@ from abogen.constants import (
LANGUAGE_DESCRIPTIONS,
VOICES_INTERNAL,
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
- COLORS
+ COLORS,
)
import threading
from abogen.voice_formula_gui import VoiceFormulaDialog
@@ -85,6 +85,7 @@ if platform.system() == "Windows":
import ctypes
import winreg
+
class DarkTitleBarEventFilter(QObject):
def __init__(self, is_windows, get_dark_mode_func, set_title_bar_dark_mode_func):
super().__init__()
@@ -100,6 +101,7 @@ class DarkTitleBarEventFilter(QObject):
self.set_title_bar_dark_mode(obj, True)
return False
+
class ShowWarningSignalEmitter(QObject): # New class to handle signal emission
show_warning_signal = pyqtSignal(str, str)
@@ -126,12 +128,16 @@ class InputBox(QLabel):
# Define CSS styles as class constants
STYLE_DEFAULT = f"border:2px dashed #aaa; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG']}; min-height:100px;"
STYLE_DEFAULT_HOVER = f"background:{COLORS['BLUE_BG_HOVER']}; border-color:{COLORS['BLUE_BORDER_HOVER']};"
-
+
STYLE_ACTIVE = f"border:2px dashed {COLORS['GREEN']}; border-radius:5px; padding:20px; background:{COLORS['GREEN_BG']}; min-height:100px;"
- STYLE_ACTIVE_HOVER = f"background:{COLORS['GREEN_BG_HOVER']}; border-color:{COLORS['GREEN_BORDER']};"
-
+ STYLE_ACTIVE_HOVER = (
+ f"background:{COLORS['GREEN_BG_HOVER']}; border-color:{COLORS['GREEN_BORDER']};"
+ )
+
STYLE_ERROR = f"border:2px dashed {COLORS['RED']}; border-radius:5px; padding:20px; background:{COLORS['RED_BG']}; min-height:100px; color:{COLORS['RED']};"
- STYLE_ERROR_HOVER = f"background:{COLORS['RED_BG_HOVER']}; border-color:{COLORS['RED']};"
+ STYLE_ERROR_HOVER = (
+ f"background:{COLORS['RED_BG_HOVER']}; border-color:{COLORS['RED']};"
+ )
def __init__(self, parent=None):
super().__init__(parent)
@@ -140,7 +146,9 @@ class InputBox(QLabel):
self.setText(
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf)"
)
- self.setStyleSheet(f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
+ )
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setCursor(Qt.PointingHandCursor)
@@ -172,7 +180,9 @@ class InputBox(QLabel):
# Add Go to folder button
self.go_to_folder_btn = QPushButton("Go to folder", self)
self.go_to_folder_btn.setStyleSheet("QPushButton { padding: 6px 10px; }")
- self.go_to_folder_btn.setToolTip("Open the folder that contains the converted file")
+ self.go_to_folder_btn.setToolTip(
+ "Open the folder that contains the converted file"
+ )
self.go_to_folder_btn.clicked.connect(self.on_go_to_folder_clicked)
self.go_to_folder_btn.hide()
@@ -215,7 +225,6 @@ class InputBox(QLabel):
return float(match.group(1))
raise ValueError(f"Invalid size format: {size_str}")
-
# Format numbers with commas
def format_num(n):
try:
@@ -265,7 +274,9 @@ class InputBox(QLabel):
)
# Set fixed width to force wrapping
self.setWordWrap(True)
- self.setStyleSheet(f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }}"
+ )
self.clear_btn.show()
is_document = self.window().selected_file_type in ["epub", "pdf"]
self.chapters_btn.setVisible(is_document)
@@ -278,7 +289,6 @@ class InputBox(QLabel):
else: # PDF - always use Pages
self.chapters_btn.setText(f"Pages ({chapter_count})")
-
# Hide textbox and show edit only for .txt files
self.textbox_btn.hide()
# Show edit button for txt files directly
@@ -296,21 +306,23 @@ class InputBox(QLabel):
self.go_to_folder_btn.show()
# Enable add to queue button only when file is accepted (input box is green)
self.resizeEvent(None)
- if hasattr(self.window(), 'btn_add_to_queue'):
+ if hasattr(self.window(), "btn_add_to_queue"):
self.window().btn_add_to_queue.setEnabled(True)
self.chapters_btn.adjustSize()
# Reset the input_box_cleared_by_queue flag after setting file info
- if hasattr(self.window(), 'input_box_cleared_by_queue'):
+ if hasattr(self.window(), "input_box_cleared_by_queue"):
self.window().input_box_cleared_by_queue = False
def set_error(self, message):
self.setText(message)
- self.setStyleSheet(f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }}"
+ )
# Show textbox button in error state as well
self.textbox_btn.show()
# Disable add to queue button on error
- if hasattr(self.window(), 'btn_add_to_queue'):
+ if hasattr(self.window(), "btn_add_to_queue"):
self.window().btn_add_to_queue.setEnabled(False)
def clear_input(self):
@@ -321,7 +333,9 @@ class InputBox(QLabel):
self.setText(
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf)"
)
- self.setStyleSheet(f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
+ )
self.clear_btn.hide()
self.chapters_btn.hide()
self.chapters_btn.setText("Chapters") # Reset text
@@ -330,10 +344,10 @@ class InputBox(QLabel):
self.edit_btn.hide()
self.go_to_folder_btn.hide()
# Disable add to queue button when input is cleared
- if hasattr(self.window(), 'btn_add_to_queue'):
+ if hasattr(self.window(), "btn_add_to_queue"):
self.window().btn_add_to_queue.setEnabled(False)
# Reset the input_box_cleared_by_queue flag after setting file info
- if hasattr(self.window(), 'input_box_cleared_by_queue'):
+ if hasattr(self.window(), "input_box_cleared_by_queue"):
self.window().input_box_cleared_by_queue = True
def _human_readable_size(self, size, decimal_places=2):
@@ -352,26 +366,42 @@ class InputBox(QLabel):
urls = event.mimeData().urls()
if urls:
ext = urls[0].toLocalFile().lower()
- if ext.endswith(".txt") or ext.endswith(".epub") or ext.endswith(".pdf"):
+ if (
+ ext.endswith(".txt")
+ or ext.endswith(".epub")
+ or ext.endswith(".pdf")
+ ):
event.acceptProposedAction()
# Set hover style based on current state
if self.styleSheet().find(self.STYLE_ACTIVE) != -1:
- self.setStyleSheet(f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }} {self.STYLE_ACTIVE_HOVER}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }} {self.STYLE_ACTIVE_HOVER}"
+ )
elif self.styleSheet().find(self.STYLE_ERROR) != -1:
- self.setStyleSheet(f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }} {self.STYLE_ERROR_HOVER}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }} {self.STYLE_ERROR_HOVER}"
+ )
else:
- self.setStyleSheet(f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }} {self.STYLE_DEFAULT_HOVER}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }} {self.STYLE_DEFAULT_HOVER}"
+ )
return
event.ignore()
def dragLeaveEvent(self, event):
# Restore the style based on current state
if self.styleSheet().find(self.STYLE_ACTIVE) != -1:
- self.setStyleSheet(f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_ACTIVE} }} QLabel:hover {{ {self.STYLE_ACTIVE_HOVER} }}"
+ )
elif self.styleSheet().find(self.STYLE_ERROR) != -1:
- self.setStyleSheet(f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_ERROR} }} QLabel:hover {{ {self.STYLE_ERROR_HOVER} }}"
+ )
else:
- self.setStyleSheet(f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}")
+ self.setStyleSheet(
+ f"QLabel {{ {self.STYLE_DEFAULT} }} QLabel:hover {{ {self.STYLE_DEFAULT_HOVER} }}"
+ )
event.accept()
def dropEvent(self, event):
@@ -432,8 +462,12 @@ class InputBox(QLabel):
win = self.window()
# win.selected_file holds the path to the text that is converted.
file_to_check = win.selected_file
-
- if file_to_check and os.path.exists(file_to_check) and os.path.isfile(file_to_check):
+
+ if (
+ file_to_check
+ and os.path.exists(file_to_check)
+ and os.path.isfile(file_to_check)
+ ):
folder_path = os.path.dirname(file_to_check)
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
else:
@@ -582,6 +616,7 @@ class TextboxDialog(QDialog):
self.update_char_count()
self.text_edit.setFocus()
+
def migrate_subtitle_format(config):
"""Convert old subtitle_format values to new internal keys."""
old_to_new = {
@@ -596,6 +631,7 @@ def migrate_subtitle_format(config):
config["subtitle_format"] = old_to_new[val]
save_config(config)
+
class abogen(QWidget):
def __init__(self):
super().__init__()
@@ -631,7 +667,9 @@ class abogen(QWidget):
"max_subtitle_words", 50
) # Default max words per subtitle
self.selected_format = self.config.get("selected_format", "wav")
- self.separate_chapters_format = self.config.get("separate_chapters_format", "wav") # Format for individual chapter files
+ self.separate_chapters_format = self.config.get(
+ "separate_chapters_format", "wav"
+ ) # Format for individual chapter files
self.use_gpu = self.config.get(
"use_gpu", True # Load GPU setting with default True
)
@@ -645,7 +683,9 @@ class abogen(QWidget):
# Create warning signal emitter
self.warning_signal_emitter = ShowWarningSignalEmitter()
- self.warning_signal_emitter.show_warning_signal.connect(self.show_model_download_warning)
+ self.warning_signal_emitter.show_warning_signal.connect(
+ self.show_model_download_warning
+ )
hf_tracker.set_show_warning_signal_emitter(self.warning_signal_emitter)
# Set application icon
@@ -793,6 +833,7 @@ class abogen(QWidget):
self.btn_voice_formula_mixer.setStyleSheet("QPushButton { padding: 6px 12px; }")
self.btn_voice_formula_mixer.clicked.connect(self.show_voice_formula_dialog)
voice_layout.addWidget(self.btn_voice_formula_mixer)
+
# Play/Stop icons
def make_icon(color, shape):
pix = QPixmap(20, 20)
@@ -900,13 +941,15 @@ class abogen(QWidget):
self.subtitle_format_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }"
)
- self.subtitle_format_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.subtitle_format_combo.setSizePolicy(
+ QSizePolicy.Expanding, QSizePolicy.Fixed
+ )
subtitle_formats = [
("srt", "SRT (standard)"),
("ass_wide", "ASS (wide)"),
("ass_narrow", "ASS (narrow)"),
("ass_centered_wide", "ASS (centered wide)"),
- ("ass_centered_narrow", "ASS (centered narrow)")
+ ("ass_centered_narrow", "ASS (centered narrow)"),
]
for value, text in subtitle_formats:
self.subtitle_format_combo.addItem(text, value)
@@ -927,13 +970,19 @@ class abogen(QWidget):
replace_newlines_layout.addWidget(replace_newlines_label)
self.replace_newlines_combo = QComboBox(self)
self.replace_newlines_combo.addItems(["Disabled", "Enabled"])
- self.replace_newlines_combo.setToolTip("Replace single newlines in the input text with spaces before processing.")
+ self.replace_newlines_combo.setToolTip(
+ "Replace single newlines in the input text with spaces before processing."
+ )
self.replace_newlines_combo.setStyleSheet(
"QComboBox { min-height: 20px; padding: 6px 12px; }"
)
- self.replace_newlines_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
+ self.replace_newlines_combo.setSizePolicy(
+ QSizePolicy.Expanding, QSizePolicy.Fixed
+ )
# Set initial value based on config
- self.replace_newlines_combo.setCurrentIndex(1 if self.replace_single_newlines else 0)
+ self.replace_newlines_combo.setCurrentIndex(
+ 1 if self.replace_single_newlines else 0
+ )
self.replace_newlines_combo.currentIndexChanged.connect(
lambda idx: self.toggle_replace_single_newlines(idx == 1)
)
@@ -969,9 +1018,7 @@ class abogen(QWidget):
selected_folder_label = QLabel("Selected folder:", self.save_path_row_widget)
save_path_row.addWidget(selected_folder_label)
self.save_path_label = QLabel("", self.save_path_row_widget)
- self.save_path_label.setStyleSheet(
- f"QLabel {{ color: {COLORS['GREEN']}; }}"
- )
+ self.save_path_label.setStyleSheet(f"QLabel {{ color: {COLORS['GREEN']}; }}")
self.save_path_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
save_path_row.addWidget(self.save_path_label)
self.save_path_row_widget.hide() # Hide the whole row by default
@@ -1118,7 +1165,8 @@ class abogen(QWidget):
)
dialog.setWindowModality(Qt.NonModal)
dialog.setModal(False)
- dialog.show() # We'll handle the dialog result asynchronously
+ dialog.show() # We'll handle the dialog result asynchronously
+
def on_dialog_finished(result):
if result != QDialog.Accepted:
return False
@@ -1142,7 +1190,7 @@ class abogen(QWidget):
# Use "abogen" prefix for temporary files
# Extract base name without extension
base_name = os.path.splitext(os.path.basename(book_path))[0]
-
+
if self.save_as_project:
# Get project directory from user
project_dir = QFileDialog.getExistingDirectory(
@@ -1158,27 +1206,54 @@ class abogen(QWidget):
project_dir = os.path.join(project_dir, project_name)
temp_dir = os.path.join(project_dir, "text")
os.makedirs(temp_dir, exist_ok=True)
-
+
# Save metadata if available
meta_dir = os.path.join(project_dir, "metadata")
- os.makedirs(meta_dir, exist_ok=True) # Save book metadata if available
+ os.makedirs(
+ meta_dir, exist_ok=True
+ ) # Save book metadata if available
if hasattr(dialog, "book_metadata"):
meta_path = os.path.join(meta_dir, "book_info.txt")
with open(meta_path, "w", encoding="utf-8") as f:
# Clean HTML tags from metadata
- title = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('title', 'Unknown')))
- publisher = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('publisher', 'Unknown')))
- authors = [re.sub(r'<[^>]+>', '', str(author)) for author in dialog.book_metadata.get('authors', ['Unknown'])]
- publication_year = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('publication_year', 'Unknown')))
-
+ title = re.sub(
+ r"<[^>]+>",
+ "",
+ str(dialog.book_metadata.get("title", "Unknown")),
+ )
+ publisher = re.sub(
+ r"<[^>]+>",
+ "",
+ str(dialog.book_metadata.get("publisher", "Unknown")),
+ )
+ authors = [
+ re.sub(r"<[^>]+>", "", str(author))
+ for author in dialog.book_metadata.get(
+ "authors", ["Unknown"]
+ )
+ ]
+ publication_year = re.sub(
+ r"<[^>]+>",
+ "",
+ str(
+ dialog.book_metadata.get(
+ "publication_year", "Unknown"
+ )
+ ),
+ )
+
f.write(f"Title: {title}\n")
f.write(f"Authors: {', '.join(authors)}\n")
f.write(f"Publisher: {publisher}\n")
f.write(f"Publication Year: {publication_year}\n")
- if dialog.book_metadata.get('description'):
- description = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('description')))
+ if dialog.book_metadata.get("description"):
+ description = re.sub(
+ r"<[^>]+>",
+ "",
+ str(dialog.book_metadata.get("description")),
+ )
f.write(f"\nDescription:\n{description}\n")
-
+
# Save cover image if available
if dialog.book_metadata.get("cover_image"):
cover_path = os.path.join(meta_dir, "cover.png")
@@ -1404,7 +1479,7 @@ class abogen(QWidget):
self._update_log_main_thread(message)
def _update_log_main_thread(self, message):
- if not hasattr(self, '_log_lines'):
+ if not hasattr(self, "_log_lines"):
self._log_lines = []
# Always produce a single HTML line (with color if tuple)
@@ -1423,7 +1498,7 @@ class abogen(QWidget):
# Trim buffer if needed
if len(self._log_lines) > self.log_window_max_lines:
- self._log_lines = self._log_lines[-self.log_window_max_lines:]
+ self._log_lines = self._log_lines[-self.log_window_max_lines :]
self.log_text.clear()
for line in self._log_lines:
self.log_text.insertHtml(line)
@@ -1438,7 +1513,11 @@ class abogen(QWidget):
def _get_queue_progress_format(self, value=None):
"""Return the progress bar format string for queue mode."""
- if hasattr(self, 'queued_items') and self.queued_items and hasattr(self, 'current_queue_index'):
+ if (
+ hasattr(self, "queued_items")
+ and self.queued_items
+ and hasattr(self, "current_queue_index")
+ ):
N = self.current_queue_index + 1
M = len(self.queued_items)
percent = value if value is not None else self.progress_bar.value()
@@ -1453,7 +1532,11 @@ class abogen(QWidget):
value = 99
self.progress_bar.setValue(value)
# Show queue progress if in queue mode
- if hasattr(self, 'queued_items') and self.queued_items and hasattr(self, 'current_queue_index'):
+ if (
+ hasattr(self, "queued_items")
+ and self.queued_items
+ and hasattr(self, "current_queue_index")
+ ):
N = self.current_queue_index + 1
M = len(self.queued_items)
self.progress_bar.setFormat(f"{value}% ({N}/{M})")
@@ -1499,9 +1582,9 @@ class abogen(QWidget):
pass
self.btn_start.clicked.connect(self.start_conversion)
- def enqueue(self, item : QueuedItem):
+ def enqueue(self, item: QueuedItem):
self.queued_items.append(item)
- #self.update_log((f"Enqueued: {item.file_name}", True))
+ # self.update_log((f"Enqueued: {item.file_name}", True))
# enable start queue button, manage queue button
self.enable_disable_queue_buttons()
@@ -1509,8 +1592,15 @@ class abogen(QWidget):
return self.queued_items
def add_to_queue(self):
- # Use the file currently displayed in the input box
- file_to_queue = self.displayed_file_path if self.displayed_file_path else self.selected_file
+ # For epub/pdf, always use the converted txt file (selected_file)
+ if self.selected_file_type in ["epub", "pdf"]:
+ file_to_queue = self.selected_file
+ else:
+ file_to_queue = (
+ self.displayed_file_path
+ if self.displayed_file_path
+ else self.selected_file
+ )
if not file_to_queue:
self.input_box.set_error("Please add a file.")
@@ -1529,23 +1619,26 @@ class abogen(QWidget):
subtitle_mode=actual_subtitle_mode,
output_format=self.selected_format,
total_char_count=self.char_count,
- replace_single_newlines=self.replace_single_newlines
+ replace_single_newlines=self.replace_single_newlines,
)
# Prevent adding duplicate items to the queue
for queued_item in self.queued_items:
if (
- queued_item.file_name == item_queue.file_name and
- queued_item.lang_code == item_queue.lang_code and
- queued_item.speed == item_queue.speed and
- queued_item.voice == item_queue.voice and
- queued_item.save_option == item_queue.save_option and
- queued_item.output_folder == item_queue.output_folder and
- queued_item.subtitle_mode == item_queue.subtitle_mode and
- queued_item.output_format == item_queue.output_format and
- getattr(queued_item, "replace_single_newlines", False) == item_queue.replace_single_newlines
+ queued_item.file_name == item_queue.file_name
+ and queued_item.lang_code == item_queue.lang_code
+ and queued_item.speed == item_queue.speed
+ and queued_item.voice == item_queue.voice
+ and queued_item.save_option == item_queue.save_option
+ and queued_item.output_folder == item_queue.output_folder
+ and queued_item.subtitle_mode == item_queue.subtitle_mode
+ and queued_item.output_format == item_queue.output_format
+ and getattr(queued_item, "replace_single_newlines", False)
+ == item_queue.replace_single_newlines
):
- QMessageBox.warning(self, "Duplicate Item", "This item is already in the queue.")
+ QMessageBox.warning(
+ self, "Duplicate Item", "This item is already in the queue."
+ )
return
self.enqueue(item_queue)
@@ -1562,7 +1655,7 @@ class abogen(QWidget):
"Confirm Clear Queue",
f"Are you sure you want to clear {len(self.queued_items)} items from the queue?",
QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No
+ QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
@@ -1598,7 +1691,9 @@ class abogen(QWidget):
self.subtitle_mode = queued_item.subtitle_mode
self.selected_format = queued_item.output_format
self.char_count = queued_item.total_char_count
- self.replace_single_newlines = getattr(queued_item, "replace_single_newlines", False)
+ self.replace_single_newlines = getattr(
+ queued_item, "replace_single_newlines", False
+ )
self.start_conversion(from_queue=True)
else:
# Queue finished, reset index
@@ -1612,7 +1707,6 @@ class abogen(QWidget):
else:
self.current_queue_index = 0 # Reset for next time
-
def get_voice_formula(self) -> str:
if self.mixed_voice_state:
formula_components = [
@@ -1625,22 +1719,19 @@ class abogen(QWidget):
def get_selected_lang(self, voice_formula) -> str:
if self.selected_profile_name:
from abogen.voice_profiles import load_profiles
+
entry = load_profiles().get(self.selected_profile_name, {})
selected_lang = entry.get("language")
else:
selected_lang = self.selected_voice[0] if self.selected_voice else None
# fallback: extract from formula if missing
if not selected_lang:
- m = re.search(r"\b([a-z])", voice_formula)
- selected_lang = m.group(1) if m else None
+ m = re.search(r"\b([a-z])", voice_formula)
+ selected_lang = m.group(1) if m else None
return selected_lang
def get_actual_subtitle_mode(self) -> str:
- return (
- "Disabled"
- if not self.subtitle_combo.isEnabled()
- else self.subtitle_mode
- )
+ return "Disabled" if not self.subtitle_combo.isEnabled() else self.subtitle_mode
def start_conversion(self, from_queue=False):
if not self.selected_file:
@@ -1651,7 +1742,12 @@ class abogen(QWidget):
self.convert_input_box_to_log()
self.progress_bar.setValue(0)
# Show queue progress if in queue mode
- if from_queue and hasattr(self, 'queued_items') and self.queued_items and hasattr(self, 'current_queue_index'):
+ if (
+ from_queue
+ and hasattr(self, "queued_items")
+ and self.queued_items
+ and hasattr(self, "current_queue_index")
+ ):
N = self.current_queue_index + 1
M = len(self.queued_items)
self.progress_bar.setFormat(f"0% ({N}/{M})")
@@ -1725,9 +1821,13 @@ class abogen(QWidget):
self.replace_single_newlines
)
# Pass separate_chapters_format setting
- self.conversion_thread.separate_chapters_format = self.separate_chapters_format
+ self.conversion_thread.separate_chapters_format = (
+ self.separate_chapters_format
+ )
# Pass subtitle format setting
- self.conversion_thread.subtitle_format = self.config.get("subtitle_format", "ass_centered_narrow")
+ self.conversion_thread.subtitle_format = self.config.get(
+ "subtitle_format", "ass_centered_narrow"
+ )
# Pass chapter count for EPUB or PDF files
if self.selected_file_type in ["epub", "pdf"] and hasattr(
self, "selected_chapters"
@@ -1780,8 +1880,7 @@ class abogen(QWidget):
f"Queue finished
"
f"Processed {len(self.queued_items)} items:
"
)
-
-
+
for idx, item in enumerate(self.queued_items, 1):
output = getattr(item, "output_path", None)
if not output:
@@ -1834,7 +1933,7 @@ class abogen(QWidget):
else self.selected_file
)
# Only repopulate if not cleared by queue
- if not getattr(self, 'input_box_cleared_by_queue', False):
+ if not getattr(self, "input_box_cleared_by_queue", False):
if display_path and os.path.exists(display_path):
self.input_box.set_file_info(display_path)
else:
@@ -1873,7 +1972,10 @@ class abogen(QWidget):
self.open_file_btn.setVisible(show_open_file_button)
# Only show finish_widget if queue is done
- if self.current_queue_index + 1 >= len(self.queued_items) or not self.queued_items:
+ if (
+ self.current_queue_index + 1 >= len(self.queued_items)
+ or not self.queued_items
+ ):
# Queue finished, show finish screen
self.controls_widget.hide()
self.finish_widget.show()
@@ -1938,7 +2040,7 @@ class abogen(QWidget):
)
# Only repopulate if not cleared by queue
- if not getattr(self, 'input_box_cleared_by_queue', False):
+ if not getattr(self, "input_box_cleared_by_queue", False):
if display_path and os.path.exists(display_path):
self.input_box.set_file_info(display_path)
else:
@@ -2031,6 +2133,7 @@ class abogen(QWidget):
voice_to_cache = voice_formula
if self.selected_profile_name:
from abogen.voice_profiles import load_profiles
+
entry = load_profiles().get(self.selected_profile_name, {})
lang_to_cache = entry.get("language")
else:
@@ -2051,12 +2154,14 @@ class abogen(QWidget):
return None
cache_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME, "preview_cache")
-
+
if "*" in voice_to_cache: # Voice formula
- voice_id = f"voice_formula_{hashlib.md5(voice_to_cache.encode()).hexdigest()[:8]}"
+ voice_id = (
+ f"voice_formula_{hashlib.md5(voice_to_cache.encode()).hexdigest()[:8]}"
+ )
else: # Single voice
voice_id = voice_to_cache
-
+
filename = f"{voice_id}_{lang_to_cache}_{speed:.2f}.wav"
return os.path.join(cache_dir, filename)
@@ -2083,7 +2188,7 @@ class abogen(QWidget):
self.voice_combo.setEnabled(False)
self.btn_voice_formula_mixer.setEnabled(False)
self.btn_start.setEnabled(False)
-
+
# Directly play from cache
self.preview_playing = True
self.btn_preview.setIcon(self.stop_icon)
@@ -2096,13 +2201,17 @@ class abogen(QWidget):
try:
# Ensure pygame mixer is initialized for the audio thread
import pygame
+
if not pygame.mixer.get_init():
pygame.mixer.init()
self.play_audio_thread = PlayAudioThread(cached_path)
self.play_audio_thread.finished.connect(cleanup_cached_play)
self.play_audio_thread.error.connect(
- lambda msg: (self._show_preview_error_box(msg), cleanup_cached_play())
+ lambda msg: (
+ self._show_preview_error_box(msg),
+ cleanup_cached_play(),
+ )
)
self.play_audio_thread.start()
except Exception as e:
@@ -2192,31 +2301,38 @@ class abogen(QWidget):
def _play_preview_audio(self, from_cache=True): # from_cache default is now False
# If preview_thread is the source, get temp_wav from it
- if hasattr(self, 'preview_thread') and not from_cache:
+ if hasattr(self, "preview_thread") and not from_cache:
temp_wav = self.preview_thread.temp_wav
elif from_cache: # This case is now handled before calling _play_preview_audio
cached_path = self._get_preview_cache_path()
if cached_path and os.path.exists(cached_path):
temp_wav = cached_path
else: # Should not happen if cache check was done
- self._show_error_message_box("Preview Error", "Cache file expected but not found, please try again.")
+ self._show_error_message_box(
+ "Preview Error",
+ "Cache file expected but not found, please try again.",
+ )
self._preview_cleanup()
return
else: # Should have temp_wav from preview_thread or handled by cache check
- self._show_error_message_box("Preview Error", "Preview audio path not found.")
+ self._show_error_message_box(
+ "Preview Error", "Preview audio path not found."
+ )
self._preview_cleanup()
return
if not temp_wav:
- if hasattr(self, 'loading_movie'): self.loading_movie.stop()
+ if hasattr(self, "loading_movie"):
+ self.loading_movie.stop()
self._show_error_message_box(
"Preview Error", "Preview error: No audio generated."
)
self._preview_cleanup()
return
-
+
# stop loading animation, switch to stop icon
- if hasattr(self, 'loading_movie'): self.loading_movie.stop()
+ if hasattr(self, "loading_movie"):
+ self.loading_movie.stop()
self.preview_playing = True
self.btn_preview.setIcon(self.stop_icon)
self.btn_preview.setToolTip("Stop preview")
@@ -2224,9 +2340,16 @@ class abogen(QWidget):
def cleanup():
# Only remove if not from cache AND it's a temp file from VoicePreviewThread
- if not from_cache and hasattr(self, 'preview_thread') and hasattr(self.preview_thread, 'temp_wav') and self.preview_thread.temp_wav == temp_wav:
+ if (
+ not from_cache
+ and hasattr(self, "preview_thread")
+ and hasattr(self.preview_thread, "temp_wav")
+ and self.preview_thread.temp_wav == temp_wav
+ ):
try:
- if os.path.exists(temp_wav): # Ensure it exists before trying to remove
+ if os.path.exists(
+ temp_wav
+ ): # Ensure it exists before trying to remove
os.remove(temp_wav)
except Exception:
pass
@@ -2235,6 +2358,7 @@ class abogen(QWidget):
try:
# Ensure pygame mixer is initialized for the audio thread
import pygame
+
if not pygame.mixer.get_init():
pygame.mixer.init()
@@ -2266,9 +2390,11 @@ class abogen(QWidget):
def _preview_cleanup(self):
self.preview_playing = False
- if hasattr(self, 'loading_movie'): self.loading_movie.stop()
+ if hasattr(self, "loading_movie"):
+ self.loading_movie.stop()
try:
- if hasattr(self, 'loading_movie'): self.loading_movie.frameChanged.disconnect()
+ if hasattr(self, "loading_movie"):
+ self.loading_movie.frameChanged.disconnect()
except Exception:
pass # Ignore error if not connected
self.btn_preview.setIcon(self.play_icon)
@@ -2301,10 +2427,12 @@ class abogen(QWidget):
):
if not hasattr(self, "_conversion_lock"):
self._conversion_lock = threading.Lock()
+
def _cancel():
with self._conversion_lock:
self.conversion_thread.cancel() # <-- Use cancel() method
self.conversion_thread.wait()
+
threading.Thread(target=_cancel, daemon=True).start()
self.is_converting = False
@@ -2323,7 +2451,7 @@ class abogen(QWidget):
else self.selected_file
)
# Only repopulate if not cleared by queue
- if not getattr(self, 'input_box_cleared_by_queue', False):
+ if not getattr(self, "input_box_cleared_by_queue", False):
if display_path and os.path.exists(display_path):
self.input_box.set_file_info(display_path)
else:
@@ -2409,9 +2537,10 @@ class abogen(QWidget):
def is_windows_dark_mode():
try:
import winreg
+
with winreg.OpenKey(
winreg.HKEY_CURRENT_USER,
- r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
+ r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
) as key:
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
return value == 0
@@ -2436,13 +2565,22 @@ class abogen(QWidget):
palette.setColor(QPalette.ColorRole.Button, button_bg)
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
# Disabled roles
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, disabled_fg)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_fg)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_fg)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, dark_bg)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Button, dark_bg)
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, disabled_fg
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_fg
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_fg
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, dark_bg
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.Button, dark_bg
+ )
app.setPalette(palette)
-
def set_light_palette():
palette = QPalette()
@@ -2457,11 +2595,25 @@ class abogen(QWidget):
palette.setColor(QPalette.ColorRole.Button, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black)
# Disabled roles
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, disabled_fg)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_fg)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_fg)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, Qt.GlobalColor.white)
- palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Button, Qt.GlobalColor.white)
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, disabled_fg
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_fg
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_fg
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled,
+ QPalette.ColorRole.Base,
+ Qt.GlobalColor.white,
+ )
+ palette.setColor(
+ QPalette.ColorGroup.Disabled,
+ QPalette.ColorRole.Button,
+ Qt.GlobalColor.white,
+ )
app.setPalette(palette)
# --- Dark title bar support for Windows ---
@@ -2473,12 +2625,19 @@ class abogen(QWidget):
set_window_attribute = ctypes.windll.dwmapi.DwmSetWindowAttribute
hwnd = int(window.winId())
value = ctypes.c_int(2 if enable else 0)
- set_window_attribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ctypes.byref(value), ctypes.sizeof(value))
+ set_window_attribute(
+ hwnd,
+ DWMWA_USE_IMMERSIVE_DARK_MODE,
+ ctypes.byref(value),
+ ctypes.sizeof(value),
+ )
except Exception:
pass
# Main logic
- dark_mode = theme == "dark" or (theme == "system" and is_windows and is_windows_dark_mode())
+ dark_mode = theme == "dark" or (
+ theme == "system" and is_windows and is_windows_dark_mode()
+ )
if dark_mode:
app.setStyle("Fusion")
set_dark_palette()
@@ -2512,7 +2671,10 @@ class abogen(QWidget):
delattr(app, "_dark_titlebar_event_filter")
def get_dark_mode():
- return theme == "dark" or (theme == "system" and is_windows and is_windows_dark_mode())
+ return theme == "dark" or (
+ theme == "system" and is_windows and is_windows_dark_mode()
+ )
+
app._dark_titlebar_event_filter = DarkTitleBarEventFilter(
is_windows, get_dark_mode, set_title_bar_dark_mode
)
@@ -2554,26 +2716,32 @@ class abogen(QWidget):
# Add separate chapters format option
separate_chapters_format_menu = QMenu("Separate chapters audio format", self)
- separate_chapters_format_menu.setToolTip("Choose the format for individual chapter files")
-
+ separate_chapters_format_menu.setToolTip(
+ "Choose the format for individual chapter files"
+ )
+
format_group = QActionGroup(self)
format_group.setExclusive(True)
-
+
for format_option in ["wav", "flac", "mp3", "opus"]:
format_action = QAction(format_option, self)
format_action.setCheckable(True)
format_action.setChecked(self.separate_chapters_format == format_option)
- format_action.triggered.connect(lambda checked, fmt=format_option: self.set_separate_chapters_format(fmt))
+ format_action.triggered.connect(
+ lambda checked, fmt=format_option: self.set_separate_chapters_format(
+ fmt
+ )
+ )
format_group.addAction(format_action)
separate_chapters_format_menu.addAction(format_action)
-
+
menu.addMenu(separate_chapters_format_menu)
# Add max words per subtitle option
max_words_action = QAction("Configure max words per subtitle", self)
max_words_action.triggered.connect(self.set_max_subtitle_words)
menu.addAction(max_words_action)
-
+
max_lines_action = QAction("Configure max lines in log window", self)
max_lines_action.triggered.connect(self.set_max_log_lines)
menu.addAction(max_lines_action)
@@ -2584,7 +2752,11 @@ class abogen(QWidget):
# Add shortcut to desktop (Windows or Linux)
if platform.system() == "Windows" or platform.system() == "Linux":
# Use extended label on Linux
- label = "Create desktop shortcut and install" if platform.system() == "Linux" else "Create desktop shortcut"
+ label = (
+ "Create desktop shortcut and install"
+ if platform.system() == "Linux"
+ else "Create desktop shortcut"
+ )
add_shortcut_action = QAction(label, self)
add_shortcut_action.triggered.connect(self.add_shortcut_to_desktop)
menu.addAction(add_shortcut_action)
@@ -2673,25 +2845,33 @@ class abogen(QWidget):
target = os.path.join(python_dir, "Scripts", "abogen.exe")
if not os.path.exists(target):
QMessageBox.critical(
- self, "Shortcut Error", f"Could not find abogen.exe at:\n{target}"
+ self,
+ "Shortcut Error",
+ f"Could not find abogen.exe at:\n{target}",
)
return
# icon (fallback to exe if missing)
icon = get_resource_path("abogen.assets", "icon.ico")
if not icon or not os.path.exists(icon):
- icon = target # Create a more direct PowerShell command
+ icon = target # Create a more direct PowerShell command
shortcut_ps = shortcut_path.replace("'", "''").replace("\\", "\\\\")
target_ps = target.replace("'", "''").replace("\\", "\\\\")
- workdir_ps = os.path.dirname(target).replace("'", "''").replace("\\", "\\\\")
+ workdir_ps = (
+ os.path.dirname(target).replace("'", "''").replace("\\", "\\\\")
+ )
icon_ps = icon.replace("'", "''").replace("\\", "\\\\")
- # Create PowerShell script as a single line with no line breaks (more reliable)
+ # Create PowerShell script as a single line with no line breaks (more reliable)
ps_cmd = f"$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('{shortcut_ps}'); $lnk.TargetPath='{target_ps}'; $lnk.WorkingDirectory='{workdir_ps}'; $lnk.IconLocation='{icon_ps}'; $lnk.Save()"
-
+
# Run PowerShell with the command directly
- proc = create_process("powershell -NoProfile -ExecutionPolicy Bypass -Command \"" + ps_cmd + "\"")
+ proc = create_process(
+ 'powershell -NoProfile -ExecutionPolicy Bypass -Command "'
+ + ps_cmd
+ + '"'
+ )
proc.wait()
-
+
if proc.returncode == 0:
QMessageBox.information(
self,
@@ -2702,17 +2882,20 @@ class abogen(QWidget):
QMessageBox.critical(
self,
"Shortcut Error",
- f"PowerShell failed with exit code: {proc.returncode}"
+ f"PowerShell failed with exit code: {proc.returncode}",
)
elif platform.system() == "Linux":
desktop = user_desktop_dir()
if not desktop or not os.path.isdir(desktop):
- QMessageBox.critical(self, "Shortcut Error", "Could not determine desktop directory.")
+ QMessageBox.critical(
+ self, "Shortcut Error", "Could not determine desktop directory."
+ )
return
shortcut_path = os.path.join(desktop, "abogen.desktop")
import shutil
+
found = shutil.which("abogen")
if found:
target = found
@@ -2728,7 +2911,11 @@ class abogen(QWidget):
if os.path.exists(target_fallback):
target = target_fallback
else:
- QMessageBox.critical(self, "Shortcut Error", "Could not find abogen executable in PATH or common installation directories.")
+ QMessageBox.critical(
+ self,
+ "Shortcut Error",
+ "Could not find abogen executable in PATH or common installation directories.",
+ )
return
icon_path = get_resource_path("abogen.assets", "icon.png")
@@ -2763,6 +2950,7 @@ Categories=AudioVideo;Audio;Utility;
)
if reply == QMessageBox.Yes:
import shutil
+
user_app_dir = os.path.expanduser("~/.local/share/applications")
os.makedirs(user_app_dir, exist_ok=True)
user_entry = os.path.join(user_app_dir, "abogen.desktop")
@@ -2782,7 +2970,9 @@ Categories=AudioVideo;Audio;Utility;
)
else:
QMessageBox.information(
- self, "Unsupported OS", "Desktop shortcut creation is not supported on this operating system."
+ self,
+ "Unsupported OS",
+ "Desktop shortcut creation is not supported on this operating system.",
)
except Exception as e:
@@ -2996,9 +3186,9 @@ Categories=AudioVideo;Audio;Utility;
if os.path.exists(preview_cache_dir):
preview_pattern = os.path.join(preview_cache_dir, "*.wav")
preview_files = glob.glob(preview_pattern)
-
+
preview_count = len(preview_files)
-
+
if file_count == 0 and preview_count == 0:
QMessageBox.information(
self, "No Temporary Files", "No temporary files were found."
@@ -3009,26 +3199,26 @@ Categories=AudioVideo;Audio;Utility;
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Question)
msg_box.setWindowTitle("Clear Temporary Files")
-
+
msg_text = f"Found {file_count} temporary file{'s' if file_count != 1 else ''} in the {PROGRAM_NAME} temp folder."
if preview_count > 0:
msg_text += f"\nAlso found {preview_count} preview cache file{'s' if preview_count != 1 else ''}."
-
+
msg_box.setText(msg_text + "\nDo you want to delete them?")
-
+
# Add checkbox for preview cache
preview_cache_checkbox = QCheckBox("Clean preview cache", msg_box)
preview_cache_checkbox.setChecked(False)
# Only enable checkbox if preview files exist
preview_cache_checkbox.setEnabled(preview_count > 0)
-
+
# Add the checkbox to the layout
msg_box.setCheckBox(preview_cache_checkbox)
-
+
# Add buttons
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
msg_box.setDefaultButton(QMessageBox.Yes)
-
+
if msg_box.exec_() != QMessageBox.Yes:
return
@@ -3040,7 +3230,7 @@ Categories=AudioVideo;Audio;Utility;
deleted_count += 1
except Exception as e:
print(f"Error deleting {file_path}: {e}")
-
+
# Delete preview cache files if checkbox is checked
deleted_preview_count = 0
if preview_cache_checkbox.isChecked() and preview_count > 0:
@@ -3055,11 +3245,9 @@ Categories=AudioVideo;Audio;Utility;
result_msg = f"Successfully deleted {deleted_count} temporary file{'s' if deleted_count != 1 else ''}."
if preview_cache_checkbox.isChecked() and deleted_preview_count > 0:
result_msg += f"\nAlso deleted {deleted_preview_count} preview cache file{'s' if deleted_preview_count != 1 else ''}."
-
+
# Show results
- QMessageBox.information(
- self, "Temporary Files Cleared", result_msg
- )
+ QMessageBox.information(self, "Temporary Files Cleared", result_msg)
# If currently selected file is in the temp directory, clear the UI
if (
@@ -3139,4 +3327,3 @@ Categories=AudioVideo;Audio;Utility;
def show_model_download_warning(self, title, message):
QMessageBox.information(self, title, message)
-
diff --git a/abogen/hf_tracker.py b/abogen/hf_tracker.py
index a0f760e..84c6350 100644
--- a/abogen/hf_tracker.py
+++ b/abogen/hf_tracker.py
@@ -1,16 +1,20 @@
log_callback = None
show_warning_signal_emitter = None # Renamed for clarity
+
def set_log_callback(cb):
global log_callback
log_callback = cb
+
def set_show_warning_signal_emitter(emitter): # Renamed for clarity
global show_warning_signal_emitter
show_warning_signal_emitter = emitter
+
from huggingface_hub import hf_hub_download
+
def tracked_hf_hub_download(*args, **kwargs):
try:
local_kwargs = dict(kwargs)
@@ -22,7 +26,10 @@ def tracked_hf_hub_download(*args, **kwargs):
if filename.endswith(".pth"):
msg = f"\nDownloading model '{filename}' from Hugging Face ({repo_id}). This may take a while. Please wait..."
if show_warning_signal_emitter: # Check if the emitter is set
- show_warning_signal_emitter.emit("Downloading Model", f"Downloading model '{filename}' from Hugging Face repository '{repo_id}'. This may take a while, please wait.")
+ show_warning_signal_emitter.emit(
+ "Downloading Model",
+ f"Downloading model '{filename}' from Hugging Face repository '{repo_id}'. This may take a while, please wait.",
+ )
else:
msg = f"\nDownloading '{filename}' from Hugging Face ({repo_id}). Please wait..."
if log_callback:
@@ -32,5 +39,7 @@ def tracked_hf_hub_download(*args, **kwargs):
print(msg, flush=True)
return hf_hub_download(*args, **kwargs)
+
import huggingface_hub
-huggingface_hub.hf_hub_download = tracked_hf_hub_download
\ No newline at end of file
+
+huggingface_hub.hf_hub_download = tracked_hf_hub_download
diff --git a/abogen/is_nvidia.py b/abogen/is_nvidia.py
index 78bd8cf..2f81351 100644
--- a/abogen/is_nvidia.py
+++ b/abogen/is_nvidia.py
@@ -1,20 +1,22 @@
import gpustat
+
def check():
try:
stats = gpustat.new_query()
except Exception:
return False
- nvidia_keywords = ['nvidia', 'rtx', 'gtx', 'quadro', 'tesla', 'titan', 'mx']
+ nvidia_keywords = ["nvidia", "rtx", "gtx", "quadro", "tesla", "titan", "mx"]
for gpu in stats.gpus:
name = gpu.name.lower()
if any(keyword in name for keyword in nvidia_keywords):
return True
return False
+
if __name__ == "__main__":
stats = gpustat.new_query()
for gpu in stats.gpus:
print(gpu.name)
- print(check())
\ No newline at end of file
+ print(check())
diff --git a/abogen/main.py b/abogen/main.py
index d1d4a41..6933249 100644
--- a/abogen/main.py
+++ b/abogen/main.py
@@ -25,6 +25,7 @@ if sys.stderr is None:
if platform.system() == "Darwin" and platform.processor() == "arm":
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
+
# Custom message handler to filter out specific Qt warnings
def qt_message_handler(mode, context, message):
if "Wayland does not support QWindow::requestActivate()" in message:
diff --git a/abogen/queue_manager_gui.py b/abogen/queue_manager_gui.py
index 8e9ab1d..d310299 100644
--- a/abogen/queue_manager_gui.py
+++ b/abogen/queue_manager_gui.py
@@ -20,6 +20,7 @@ from abogen.constants import COLORS
from copy import deepcopy
from PyQt5.QtGui import QFontMetrics
+
class ElidedLabel(QLabel):
def __init__(self, text, parent=None):
super().__init__(text, parent)
@@ -40,6 +41,7 @@ class ElidedLabel(QLabel):
def fullText(self):
return self._full_text
+
class QueueListItemWidget(QWidget):
def __init__(self, file_name, char_count):
super().__init__()
@@ -47,6 +49,7 @@ class QueueListItemWidget(QWidget):
layout.setContentsMargins(12, 0, 6, 0)
layout.setSpacing(0)
import os
+
name_label = ElidedLabel(os.path.basename(file_name))
char_label = QLabel(f"Chars: {char_count}")
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
@@ -56,6 +59,7 @@ class QueueListItemWidget(QWidget):
layout.addWidget(char_label, 0)
self.setLayout(layout)
+
class DroppableQueueListWidget(QListWidget):
def __init__(self, parent_dialog):
super().__init__()
@@ -73,7 +77,7 @@ class DroppableQueueListWidget(QListWidget):
def dragEnterEvent(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
- if url.isLocalFile() and url.toLocalFile().lower().endswith('.txt'):
+ if url.isLocalFile() and url.toLocalFile().lower().endswith(".txt"):
self.drag_overlay.resize(self.size())
self.drag_overlay.setVisible(True)
event.acceptProposedAction()
@@ -84,7 +88,7 @@ class DroppableQueueListWidget(QListWidget):
def dragMoveEvent(self, event):
if event.mimeData().hasUrls():
for url in event.mimeData().urls():
- if url.isLocalFile() and url.toLocalFile().lower().endswith('.txt'):
+ if url.isLocalFile() and url.toLocalFile().lower().endswith(".txt"):
event.acceptProposedAction()
return
event.ignore()
@@ -96,7 +100,11 @@ class DroppableQueueListWidget(QListWidget):
def dropEvent(self, event):
self.drag_overlay.setVisible(False)
if event.mimeData().hasUrls():
- file_paths = [url.toLocalFile() for url in event.mimeData().urls() if url.isLocalFile() and url.toLocalFile().lower().endswith('.txt')]
+ file_paths = [
+ url.toLocalFile()
+ for url in event.mimeData().urls()
+ if url.isLocalFile() and url.toLocalFile().lower().endswith(".txt")
+ ]
if file_paths:
self.parent_dialog.add_files_from_paths(file_paths)
event.acceptProposedAction()
@@ -107,14 +115,17 @@ class DroppableQueueListWidget(QListWidget):
def resizeEvent(self, event):
super().resizeEvent(event)
- if hasattr(self, 'drag_overlay'):
+ if hasattr(self, "drag_overlay"):
self.drag_overlay.resize(self.size())
+
class QueueManager(QDialog):
def __init__(self, parent, queue: list, title="Queue Manager", size=(600, 700)):
super().__init__()
self.queue = queue
- self._original_queue = deepcopy(queue) # Store a deep copy of the original queue
+ self._original_queue = deepcopy(
+ queue
+ ) # Store a deep copy of the original queue
self.parent = parent
layout = QVBoxLayout()
layout.setContentsMargins(15, 15, 15, 15) # set main layout margins
@@ -141,10 +152,12 @@ class QueueManager(QDialog):
# Overlay label for empty queue
self.empty_overlay = QLabel(
"Drag and drop your text files here or use the 'Add files' button.",
- self.listwidget
+ self.listwidget,
)
self.empty_overlay.setAlignment(Qt.AlignCenter)
- self.empty_overlay.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;")
+ self.empty_overlay.setStyleSheet(
+ f"color: {COLORS['LIGHT_DISABLED']}; background: transparent; padding: 20px;"
+ )
self.empty_overlay.setWordWrap(True)
self.empty_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
self.empty_overlay.hide()
@@ -208,13 +221,14 @@ class QueueManager(QDialog):
file_name = item.file_name
display_name = file_name
import os
+
if os.path.sep in file_name:
display_name = os.path.basename(file_name)
# Get icon for the file
icon = icon_provider.icon(QFileInfo(file_name))
list_item = QListWidgetItem()
# Set tooltip with detailed info
- output_folder = getattr(item, 'output_folder', '')
+ output_folder = getattr(item, "output_folder", "")
tooltip = (
f"Path: {file_name}
"
f"Language: {getattr(item, 'lang_code', '')}
"
@@ -222,7 +236,7 @@ class QueueManager(QDialog):
f"Voice: {getattr(item, 'voice', '')}
"
f"Save Option: {getattr(item, 'save_option', '')}
"
)
- if output_folder not in (None, '', 'None'):
+ if output_folder not in (None, "", "None"):
tooltip += f"Output Folder: {output_folder}
"
tooltip += (
f"Subtitle Mode: {getattr(item, 'subtitle_mode', '')}
"
@@ -233,7 +247,7 @@ class QueueManager(QDialog):
list_item.setToolTip(tooltip)
list_item.setIcon(icon)
# Use custom widget for display
- char_count = getattr(item, 'total_char_count', 0)
+ char_count = getattr(item, "total_char_count", 0)
widget = QueueListItemWidget(file_name, char_count)
self.listwidget.addItem(list_item)
self.listwidget.setItemWidget(list_item, widget)
@@ -244,6 +258,7 @@ class QueueManager(QDialog):
if not items:
return
from PyQt5.QtWidgets import QMessageBox
+
# Remove by index to ensure correct mapping
rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
# Warn user if removing multiple files
@@ -253,7 +268,7 @@ class QueueManager(QDialog):
"Confirm Remove",
f"Are you sure you want to remove {len(rows)} selected items from the queue?",
QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No
+ QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
@@ -265,19 +280,22 @@ class QueueManager(QDialog):
def clear_queue(self):
from PyQt5.QtWidgets import QMessageBox
+
if len(self.queue) > 1:
reply = QMessageBox.question(
self,
"Confirm Clear Queue",
f"Are you sure you want to clear {len(self.queue)} items from the queue?",
QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No
+ QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
self.queue.clear()
self.listwidget.clear()
- self.empty_overlay.resize(self.listwidget.size()) # Ensure overlay is sized correctly
+ self.empty_overlay.resize(
+ self.listwidget.size()
+ ) # Ensure overlay is sized correctly
self.empty_overlay.show() # Show the overlay when queue is empty
self.update_button_states()
@@ -290,56 +308,74 @@ class QueueManager(QDialog):
parent = self.parent
if parent is not None:
# lang_code: use parent's get_voice_formula and get_selected_lang
- if hasattr(parent, 'get_voice_formula') and hasattr(parent, 'get_selected_lang'):
+ if hasattr(parent, "get_voice_formula") and hasattr(
+ parent, "get_selected_lang"
+ ):
voice_formula = parent.get_voice_formula()
- attrs['lang_code'] = parent.get_selected_lang(voice_formula)
- attrs['voice'] = voice_formula
+ attrs["lang_code"] = parent.get_selected_lang(voice_formula)
+ attrs["voice"] = voice_formula
else:
- attrs['lang_code'] = getattr(parent, 'selected_lang', '')
- attrs['voice'] = getattr(parent, 'selected_voice', '')
+ attrs["lang_code"] = getattr(parent, "selected_lang", "")
+ attrs["voice"] = getattr(parent, "selected_voice", "")
# speed
- if hasattr(parent, 'speed_slider'):
- attrs['speed'] = parent.speed_slider.value() / 100.0
+ if hasattr(parent, "speed_slider"):
+ attrs["speed"] = parent.speed_slider.value() / 100.0
else:
- attrs['speed'] = getattr(parent, 'speed', 1.0)
+ attrs["speed"] = getattr(parent, "speed", 1.0)
# save_option
- attrs['save_option'] = getattr(parent, 'save_option', '')
+ attrs["save_option"] = getattr(parent, "save_option", "")
# output_folder
- attrs['output_folder'] = getattr(parent, 'selected_output_folder', '')
+ attrs["output_folder"] = getattr(parent, "selected_output_folder", "")
# subtitle_mode
- if hasattr(parent, 'get_actual_subtitle_mode'):
- attrs['subtitle_mode'] = parent.get_actual_subtitle_mode()
+ if hasattr(parent, "get_actual_subtitle_mode"):
+ attrs["subtitle_mode"] = parent.get_actual_subtitle_mode()
else:
- attrs['subtitle_mode'] = getattr(parent, 'subtitle_mode', '')
+ attrs["subtitle_mode"] = getattr(parent, "subtitle_mode", "")
# output_format
- attrs['output_format'] = getattr(parent, 'selected_format', '')
+ attrs["output_format"] = getattr(parent, "selected_format", "")
# total_char_count
- attrs['total_char_count'] = getattr(parent, 'char_count', '')
+ attrs["total_char_count"] = getattr(parent, "char_count", "")
# replace_single_newlines
- attrs['replace_single_newlines'] = getattr(parent, 'replace_single_newlines', False)
+ attrs["replace_single_newlines"] = getattr(
+ parent, "replace_single_newlines", False
+ )
else:
# fallback: empty values
- attrs = {k: '' for k in [
- 'lang_code', 'speed', 'voice', 'save_option',
- 'output_folder', 'subtitle_mode', 'output_format', 'total_char_count', 'replace_single_newlines']}
+ attrs = {
+ k: ""
+ for k in [
+ "lang_code",
+ "speed",
+ "voice",
+ "save_option",
+ "output_folder",
+ "subtitle_mode",
+ "output_format",
+ "total_char_count",
+ "replace_single_newlines",
+ ]
+ }
return attrs
def add_files_from_paths(self, file_paths):
from abogen.utils import calculate_text_length
from PyQt5.QtWidgets import QMessageBox
import os
+
current_attrs = self.get_current_attributes()
duplicates = []
for file_path in file_paths:
+
class QueueItem:
pass
+
item = QueueItem()
item.file_name = file_path
for attr, value in current_attrs.items():
setattr(item, attr, value)
# Read file content and calculate total_char_count using calculate_text_length
try:
- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
file_content = f.read()
item.total_char_count = calculate_text_length(file_content)
except Exception:
@@ -348,16 +384,26 @@ class QueueManager(QDialog):
is_duplicate = False
for queued_item in self.queue:
if (
- getattr(queued_item, 'file_name', None) == getattr(item, 'file_name', None) and
- getattr(queued_item, 'lang_code', None) == getattr(item, 'lang_code', None) and
- getattr(queued_item, 'speed', None) == getattr(item, 'speed', None) and
- getattr(queued_item, 'voice', None) == getattr(item, 'voice', None) and
- getattr(queued_item, 'save_option', None) == getattr(item, 'save_option', None) and
- getattr(queued_item, 'output_folder', None) == getattr(item, 'output_folder', None) and
- getattr(queued_item, 'subtitle_mode', None) == getattr(item, 'subtitle_mode', None) and
- getattr(queued_item, 'output_format', None) == getattr(item, 'output_format', None) and
- getattr(queued_item, 'total_char_count', None) == getattr(item, 'total_char_count', None) and
- getattr(queued_item, 'replace_single_newlines', False) == getattr(item, 'replace_single_newlines', False)
+ getattr(queued_item, "file_name", None)
+ == getattr(item, "file_name", None)
+ and getattr(queued_item, "lang_code", None)
+ == getattr(item, "lang_code", None)
+ and getattr(queued_item, "speed", None)
+ == getattr(item, "speed", None)
+ and getattr(queued_item, "voice", None)
+ == getattr(item, "voice", None)
+ and getattr(queued_item, "save_option", None)
+ == getattr(item, "save_option", None)
+ and getattr(queued_item, "output_folder", None)
+ == getattr(item, "output_folder", None)
+ and getattr(queued_item, "subtitle_mode", None)
+ == getattr(item, "subtitle_mode", None)
+ and getattr(queued_item, "output_format", None)
+ == getattr(item, "output_format", None)
+ and getattr(queued_item, "total_char_count", None)
+ == getattr(item, "total_char_count", None)
+ and getattr(queued_item, "replace_single_newlines", False)
+ == getattr(item, "replace_single_newlines", False)
):
is_duplicate = True
break
@@ -369,7 +415,7 @@ class QueueManager(QDialog):
QMessageBox.warning(
self,
"Duplicate Item(s)",
- f"Skipping {len(duplicates)} file(s) with the same attributes, already in the queue."
+ f"Skipping {len(duplicates)} file(s) with the same attributes, already in the queue.",
)
self.process_queue()
self.update_button_states()
@@ -377,20 +423,23 @@ class QueueManager(QDialog):
def add_more_files(self):
from PyQt5.QtWidgets import QFileDialog
from abogen.utils import calculate_text_length # import the function
+
# Only allow .txt files
- files, _ = QFileDialog.getOpenFileNames(self, "Select .txt files", "", "Text Files (*.txt)")
+ files, _ = QFileDialog.getOpenFileNames(
+ self, "Select .txt files", "", "Text Files (*.txt)"
+ )
if not files:
return
self.add_files_from_paths(files)
def resizeEvent(self, event):
super().resizeEvent(event)
- if hasattr(self, 'empty_overlay'):
+ if hasattr(self, "empty_overlay"):
self.empty_overlay.resize(self.listwidget.size())
def update_button_states(self):
# Enable Remove if at least one item is selected, else disable
- if hasattr(self, 'remove_button'):
+ if hasattr(self, "remove_button"):
selected_count = len(self.listwidget.selectedItems())
self.remove_button.setEnabled(selected_count > 0)
if selected_count > 1:
@@ -398,7 +447,7 @@ class QueueManager(QDialog):
else:
self.remove_button.setText("Remove selected")
# Disable Clear if queue is empty
- if hasattr(self, 'clear_button'):
+ if hasattr(self, "clear_button"):
self.clear_button.setEnabled(bool(self.queue))
def show_context_menu(self, pos):
@@ -406,6 +455,7 @@ class QueueManager(QDialog):
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import QUrl
import os
+
global_pos = self.listwidget.viewport().mapToGlobal(pos)
selected_items = self.listwidget.selectedItems()
menu = QMenu(self)
@@ -417,35 +467,45 @@ class QueueManager(QDialog):
# Add Open file action
open_file_action = QAction("Open file", self)
+
def open_file():
from PyQt5.QtWidgets import QMessageBox
+
item = selected_items[0]
display_name = item.text()
for q in self.queue:
if os.path.basename(q.file_name) == display_name:
if not os.path.exists(q.file_name):
- QMessageBox.warning(self, "File Not Found", f"The file does not exist.")
+ QMessageBox.warning(
+ self, "File Not Found", f"The file does not exist."
+ )
return
QDesktopServices.openUrl(QUrl.fromLocalFile(q.file_name))
break
+
open_file_action.triggered.connect(open_file)
menu.addAction(open_file_action)
# Add Go to folder action
go_to_folder_action = QAction("Go to folder", self)
+
def go_to_folder():
from PyQt5.QtWidgets import QMessageBox
+
item = selected_items[0]
display_name = item.text()
for q in self.queue:
if os.path.basename(q.file_name) == display_name:
if not os.path.exists(q.file_name):
- QMessageBox.warning(self, "File Not Found", f"The file does not exist.")
+ QMessageBox.warning(
+ self, "File Not Found", f"The file does not exist."
+ )
return
folder = os.path.dirname(q.file_name)
if os.path.exists(folder):
QDesktopServices.openUrl(QUrl.fromLocalFile(folder))
break
+
go_to_folder_action.triggered.connect(go_to_folder)
menu.addAction(go_to_folder_action)
@@ -466,6 +526,7 @@ class QueueManager(QDialog):
def reject(self):
# Cancel: restore original queue
from PyQt5.QtWidgets import QMessageBox
+
# Warn if user changed a lot (e.g., more than 1 items difference)
original_count = len(self._original_queue)
current_count = len(self.queue)
@@ -475,7 +536,7 @@ class QueueManager(QDialog):
"Confirm Cancel",
f"Are you sure you want to cancel and discard all changes?",
QMessageBox.Yes | QMessageBox.No,
- QMessageBox.No
+ QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
@@ -485,7 +546,8 @@ class QueueManager(QDialog):
def keyPressEvent(self, event):
from PyQt5.QtCore import Qt
+
if event.key() == Qt.Key_Delete:
self.remove_item()
else:
- super().keyPressEvent(event)
\ No newline at end of file
+ super().keyPressEvent(event)
diff --git a/abogen/queued_item.py b/abogen/queued_item.py
index 9e676a2..9c9fd00 100644
--- a/abogen/queued_item.py
+++ b/abogen/queued_item.py
@@ -1,6 +1,7 @@
# represents a queued item - book, chapters, voice, etc.
from dataclasses import dataclass
+
@dataclass
class QueuedItem:
file_name: str
@@ -12,4 +13,4 @@ class QueuedItem:
subtitle_mode: str
output_format: str
total_char_count: int
- replace_single_newlines: bool = False
\ No newline at end of file
+ replace_single_newlines: bool = False
diff --git a/abogen/utils.py b/abogen/utils.py
index fd24176..8c32c33 100644
--- a/abogen/utils.py
+++ b/abogen/utils.py
@@ -105,19 +105,21 @@ def clean_text(text, *args, **kwargs):
default_encoding = sys.getfilesystemencoding()
+
def create_process(cmd, stdin=None, text=True, capture_output=False):
import logging
+
logger = logging.getLogger(__name__)
-
+
# Configure root logger to output to console if not already configured
root = logging.getLogger()
if not root.handlers:
handler = logging.StreamHandler(sys.stdout)
- formatter = logging.Formatter('%(message)s')
+ formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
root.addHandler(handler)
root.setLevel(logging.INFO)
-
+
# Determine shell usage: use shell only for string commands
use_shell = isinstance(cmd, str)
kwargs = {
@@ -151,11 +153,12 @@ def create_process(cmd, stdin=None, text=True, capture_output=False):
# Print the command being executed
print(f"Executing: {cmd if isinstance(cmd, str) else ' '.join(cmd)}")
-
+
proc = subprocess.Popen(cmd, **kwargs)
-
+
# Stream output to console in real-time if not capturing
if proc.stdout and not capture_output:
+
def _stream_output(stream):
if text:
# For text mode, read character by character for real-time output
@@ -174,12 +177,14 @@ def create_process(cmd, stdin=None, text=True, capture_output=False):
break
try:
# Try to decode binary data for display
- sys.stdout.write(chunk.decode(default_encoding, errors='replace'))
+ sys.stdout.write(
+ chunk.decode(default_encoding, errors="replace")
+ )
sys.stdout.flush()
except Exception:
pass
stream.close()
-
+
# Start a daemon thread to handle output streaming
Thread(target=_stream_output, args=(proc.stdout,), daemon=True).start()
@@ -220,6 +225,7 @@ def get_gpu_acceleration(enabled):
try:
import torch
from torch.cuda import is_available
+
if not enabled:
return "CUDA GPU available but using CPU.", False
if is_available():
@@ -227,7 +233,11 @@ def get_gpu_acceleration(enabled):
# Gather more diagnostic info if CUDA is not available
try:
cuda_devices = torch.cuda.device_count()
- cuda_error = torch.cuda.get_device_name(0) if cuda_devices > 0 else "No devices found"
+ cuda_error = (
+ torch.cuda.get_device_name(0)
+ if cuda_devices > 0
+ else "No devices found"
+ )
except Exception as e:
cuda_error = str(e)
return f"CUDA GPU is not available. Using CPU. ({cuda_error})", False
@@ -239,6 +249,7 @@ def prevent_sleep_start():
system = platform.system()
if system == "Windows":
import ctypes
+
ctypes.windll.kernel32.SetThreadExecutionState(
0x80000000 | 0x00000001 | 0x00000040
)
@@ -246,19 +257,22 @@ def prevent_sleep_start():
_sleep_procs["Darwin"] = create_process(["caffeinate"])
elif system == "Linux":
# use a sleep that never exits so inhibition stays active
- _sleep_procs["Linux"] = create_process([
- "systemd-inhibit",
- "--what=handle-lid-switch:sleep",
- "--mode=block",
- "sleep",
- "infinity",
- ])
+ _sleep_procs["Linux"] = create_process(
+ [
+ "systemd-inhibit",
+ "--what=handle-lid-switch:sleep",
+ "--mode=block",
+ "sleep",
+ "infinity",
+ ]
+ )
def prevent_sleep_end():
system = platform.system()
if system == "Windows":
import ctypes
+
ctypes.windll.kernel32.SetThreadExecutionState(0x80000000) # ES_CONTINUOUS
elif system in ("Darwin", "Linux") and _sleep_procs[system]:
try:
diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py
index a6e09aa..ae361a3 100644
--- a/abogen/voice_formula_gui.py
+++ b/abogen/voice_formula_gui.py
@@ -230,13 +230,15 @@ class VoiceMixer(QWidget):
appstyle = QApplication.instance().style().objectName().lower()
if appstyle != "windowsvista":
# Set custom groove color for disabled state using COLORS["GREY_BACKGROUND"]
- self.slider.setStyleSheet(f"""
+ self.slider.setStyleSheet(
+ f"""
QSlider::groove:vertical:disabled {{
background: {COLORS.get("GREY_BACKGROUND")};
width: 4px;
border-radius: 4px;
}}
- """)
+ """
+ )
# Connect controls
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
@@ -330,7 +332,9 @@ class VoiceFormulaDialog(QDialog):
self._original_mixed_voice_state = None
if parent is not None:
self._original_profile_name = getattr(parent, "selected_profile_name", None)
- self._original_mixed_voice_state = getattr(parent, "mixed_voice_state", None)
+ self._original_mixed_voice_state = getattr(
+ parent, "mixed_voice_state", None
+ )
profiles = load_profiles()
self._virtual_new_profile = False
if not profiles:
@@ -369,7 +373,9 @@ class VoiceFormulaDialog(QDialog):
self.profile_list = QListWidget()
self.profile_list.setSelectionMode(QListWidget.SingleSelection)
self.profile_list.setSelectionBehavior(QListWidget.SelectRows)
- self.profile_list.setStyleSheet("QListWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }")
+ self.profile_list.setStyleSheet(
+ "QListWidget::item:selected { background: palette(highlight); color: palette(highlighted-text); }"
+ )
icon = QIcon(get_resource_path("abogen.assets", "profile.png"))
if self._virtual_new_profile:
item = QListWidgetItem(icon, "New profile")
@@ -976,6 +982,7 @@ class VoiceFormulaDialog(QDialog):
def _parse_rgba_to_qcolor(self, rgba_str):
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor
+
"""Helper to convert 'rgba(R,G,B,A_float)' string to QColor."""
match = re.match(r"rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)", rgba_str)
if match:
@@ -1372,6 +1379,7 @@ class VoiceFormulaDialog(QDialog):
def update_profile_list_colors(self):
from PyQt5.QtCore import Qt
+
profiles = load_profiles()
for i in range(self.profile_list.count()):
item = self.profile_list.item(i)
@@ -1383,7 +1391,9 @@ class VoiceFormulaDialog(QDialog):
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
item.setData(Qt.BackgroundRole, color)
else:
- item.setData(Qt.BackgroundRole, self.profile_list.palette().base().color())
+ item.setData(
+ Qt.BackgroundRole, self.profile_list.palette().base().color()
+ )
weights = profiles.get(name, {}).get("voices", [])
total = 0
if isinstance(weights, list):
diff --git a/abogen/voice_formulas.py b/abogen/voice_formulas.py
index af18c52..813d27c 100644
--- a/abogen/voice_formulas.py
+++ b/abogen/voice_formulas.py
@@ -1,6 +1,7 @@
import re
from abogen.constants import VOICES_INTERNAL
+
# Calls parsing and loads the voice to gpu or cpu
def get_new_voice(pipeline, formula, use_gpu):
try: