mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
v1.1.1
This commit is contained in:
@@ -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
|
# 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 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
|
- Added a feature that allows selecting multiple items in book handler (in right click menu) by @jborza in #31, that fixes #28
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.1.0
|
1.1.1
|
||||||
+64
-20
@@ -997,10 +997,21 @@ class HandlerDialog(QDialog):
|
|||||||
bookmark_item.setData(0, Qt.UserRole, page_id)
|
bookmark_item.setData(0, Qt.UserRole, page_id)
|
||||||
# only allow checking if this chapter has content
|
# only allow checking if this chapter has content
|
||||||
if self.content_lengths.get(page_id, 0) > 0:
|
if self.content_lengths.get(page_id, 0) > 0:
|
||||||
bookmark_item.setFlags(bookmark_item.flags() | Qt.ItemIsUserCheckable)
|
bookmark_item.setFlags(
|
||||||
bookmark_item.setCheckState(0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked)
|
bookmark_item.flags() | Qt.ItemIsUserCheckable
|
||||||
|
)
|
||||||
|
bookmark_item.setCheckState(
|
||||||
|
0,
|
||||||
|
(
|
||||||
|
Qt.Checked
|
||||||
|
if page_id in self.checked_chapters
|
||||||
|
else Qt.Unchecked
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
bookmark_item.setFlags(bookmark_item.flags() & ~Qt.ItemIsUserCheckable)
|
bookmark_item.setFlags(
|
||||||
|
bookmark_item.flags() & ~Qt.ItemIsUserCheckable
|
||||||
|
)
|
||||||
# map for uncategorized pages
|
# map for uncategorized pages
|
||||||
self.bookmark_items_map[page_num] = bookmark_item
|
self.bookmark_items_map[page_num] = bookmark_item
|
||||||
|
|
||||||
@@ -1027,10 +1038,21 @@ class HandlerDialog(QDialog):
|
|||||||
page_item.setData(0, Qt.UserRole, page_id)
|
page_item.setData(0, Qt.UserRole, page_id)
|
||||||
# only allow checking if this sub-page has content
|
# only allow checking if this sub-page has content
|
||||||
if self.content_lengths.get(page_id, 0) > 0:
|
if self.content_lengths.get(page_id, 0) > 0:
|
||||||
page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable)
|
page_item.setFlags(
|
||||||
page_item.setCheckState(0, Qt.Checked if page_id in self.checked_chapters else Qt.Unchecked)
|
page_item.flags() | Qt.ItemIsUserCheckable
|
||||||
|
)
|
||||||
|
page_item.setCheckState(
|
||||||
|
0,
|
||||||
|
(
|
||||||
|
Qt.Checked
|
||||||
|
if page_id in self.checked_chapters
|
||||||
|
else Qt.Unchecked
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
|
page_item.setFlags(
|
||||||
|
page_item.flags() & ~Qt.ItemIsUserCheckable
|
||||||
|
)
|
||||||
|
|
||||||
added_pages.add(sub_page_num)
|
added_pages.add(sub_page_num)
|
||||||
|
|
||||||
@@ -1038,11 +1060,15 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
covered_pages = set(added_pages)
|
covered_pages = set(added_pages)
|
||||||
# attach any pages without direct bookmarks under nearest preceding chapter
|
# 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:
|
for page_num in uncategorized_pages:
|
||||||
# find nearest previous bookmark
|
# find nearest previous bookmark
|
||||||
prev_nums = [n for n in sorted(self.bookmark_items_map) if n < page_num]
|
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}"
|
page_id = f"page_{page_num+1}"
|
||||||
title = f"Page {page_num+1}"
|
title = f"Page {page_num+1}"
|
||||||
text = self.content_texts.get(page_id, "").strip()
|
text = self.content_texts.get(page_id, "").strip()
|
||||||
@@ -1055,7 +1081,9 @@ class HandlerDialog(QDialog):
|
|||||||
# only allow checking if uncategorized page has content
|
# only allow checking if uncategorized page has content
|
||||||
if self.content_lengths.get(page_id, 0) > 0:
|
if self.content_lengths.get(page_id, 0) > 0:
|
||||||
page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable)
|
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:
|
else:
|
||||||
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
|
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
|
||||||
|
|
||||||
@@ -1081,7 +1109,9 @@ class HandlerDialog(QDialog):
|
|||||||
# only allow checking if standalone page has content
|
# only allow checking if standalone page has content
|
||||||
if self.content_lengths.get(page_id, 0) > 0:
|
if self.content_lengths.get(page_id, 0) > 0:
|
||||||
page_item.setFlags(page_item.flags() | Qt.ItemIsUserCheckable)
|
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:
|
else:
|
||||||
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
|
page_item.setFlags(page_item.flags() & ~Qt.ItemIsUserCheckable)
|
||||||
|
|
||||||
@@ -1501,7 +1531,9 @@ class HandlerDialog(QDialog):
|
|||||||
authors_text = ", ".join(self.book_metadata["authors"])
|
authors_text = ", ".join(self.book_metadata["authors"])
|
||||||
html_content += f"<p style='text-align: center; font-style: italic;'>By {authors_text}</p>"
|
html_content += f"<p style='text-align: center; font-style: italic;'>By {authors_text}</p>"
|
||||||
|
|
||||||
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 = []
|
pub_info = []
|
||||||
if self.book_metadata["publisher"]:
|
if self.book_metadata["publisher"]:
|
||||||
pub_info.append(f"Published by {self.book_metadata['publisher']}")
|
pub_info.append(f"Published by {self.book_metadata['publisher']}")
|
||||||
@@ -1543,7 +1575,9 @@ class HandlerDialog(QDialog):
|
|||||||
try:
|
try:
|
||||||
author_items = self.book.get_metadata("DC", "creator")
|
author_items = self.book.get_metadata("DC", "creator")
|
||||||
if author_items:
|
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:
|
except Exception as e:
|
||||||
logging.warning(f"Error extracting author metadata: {e}")
|
logging.warning(f"Error extracting author metadata: {e}")
|
||||||
|
|
||||||
@@ -1567,7 +1601,7 @@ class HandlerDialog(QDialog):
|
|||||||
if date_items and len(date_items) > 0:
|
if date_items and len(date_items) > 0:
|
||||||
date_str = date_items[0][0]
|
date_str = date_items[0][0]
|
||||||
# Try to extract just the year from the date string
|
# 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:
|
if year_match:
|
||||||
metadata["publication_year"] = year_match.group(0)
|
metadata["publication_year"] = year_match.group(0)
|
||||||
else:
|
else:
|
||||||
@@ -1607,12 +1641,12 @@ class HandlerDialog(QDialog):
|
|||||||
# Try to extract publication date from PDF metadata
|
# Try to extract publication date from PDF metadata
|
||||||
if "creationDate" in pdf_info:
|
if "creationDate" in pdf_info:
|
||||||
date_str = pdf_info["creationDate"]
|
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:
|
if year_match:
|
||||||
metadata["publication_year"] = year_match.group(1)
|
metadata["publication_year"] = year_match.group(1)
|
||||||
elif "modDate" in pdf_info:
|
elif "modDate" in pdf_info:
|
||||||
date_str = pdf_info["modDate"]
|
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:
|
if year_match:
|
||||||
metadata["publication_year"] = year_match.group(1)
|
metadata["publication_year"] = year_match.group(1)
|
||||||
|
|
||||||
@@ -1644,11 +1678,15 @@ class HandlerDialog(QDialog):
|
|||||||
authors = metadata.get("authors") or ["Unknown"]
|
authors = metadata.get("authors") or ["Unknown"]
|
||||||
authors_text = ", ".join(authors)
|
authors_text = ", ".join(authors)
|
||||||
album_artist = authors_text or "Unknown"
|
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
|
# Count chapters/pages
|
||||||
total_chapters = len(self.checked_chapters)
|
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
|
# Format metadata tags
|
||||||
metadata_tags = [
|
metadata_tags = [
|
||||||
@@ -1658,7 +1696,7 @@ class HandlerDialog(QDialog):
|
|||||||
f"<<METADATA_YEAR:{year}>>",
|
f"<<METADATA_YEAR:{year}>>",
|
||||||
f"<<METADATA_ALBUM_ARTIST:{album_artist}>>",
|
f"<<METADATA_ALBUM_ARTIST:{album_artist}>>",
|
||||||
f"<<METADATA_COMPOSER:Narrator>>",
|
f"<<METADATA_COMPOSER:Narrator>>",
|
||||||
f"<<METADATA_GENRE:Audiobook>>"
|
f"<<METADATA_GENRE:Audiobook>>",
|
||||||
]
|
]
|
||||||
|
|
||||||
return "\n".join(metadata_tags)
|
return "\n".join(metadata_tags)
|
||||||
@@ -1730,7 +1768,10 @@ class HandlerDialog(QDialog):
|
|||||||
if text:
|
if text:
|
||||||
all_content.append(text)
|
all_content.append(text)
|
||||||
included_text_ids.add(page_id)
|
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)
|
iterator = QTreeWidgetItemIterator(self.treeWidget)
|
||||||
while iterator.value():
|
while iterator.value():
|
||||||
@@ -1788,7 +1829,10 @@ class HandlerDialog(QDialog):
|
|||||||
included_text_ids.add(identifier)
|
included_text_ids.add(identifier)
|
||||||
iterator += 1
|
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):
|
def on_save_chapters_changed(self, state):
|
||||||
self.save_chapters_separately = bool(state)
|
self.save_chapters_separately = bool(state)
|
||||||
|
|||||||
+219
-79
@@ -10,13 +10,20 @@ from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer
|
|||||||
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
from abogen.utils import clean_text, create_process
|
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
|
from abogen.voice_formulas import get_new_voice
|
||||||
import abogen.hf_tracker as hf_tracker
|
import abogen.hf_tracker as hf_tracker
|
||||||
import static_ffmpeg
|
import static_ffmpeg
|
||||||
import threading # for efficient waiting
|
import threading # for efficient waiting
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
def get_sample_voice_text(lang_code):
|
def get_sample_voice_text(lang_code):
|
||||||
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
|
return SAMPLE_VOICE_TEXTS.get(lang_code, SAMPLE_VOICE_TEXTS["a"])
|
||||||
|
|
||||||
@@ -73,7 +80,9 @@ class ChapterOptionsDialog(QDialog):
|
|||||||
|
|
||||||
# Countdown label
|
# Countdown label
|
||||||
self.countdown_seconds = CHAPTER_OPTIONS_COUNTDOWN
|
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']};")
|
self.countdown_label.setStyleSheet(f"color: {COLORS['GREEN']};")
|
||||||
layout.addWidget(self.countdown_label)
|
layout.addWidget(self.countdown_label)
|
||||||
|
|
||||||
@@ -96,7 +105,9 @@ class ChapterOptionsDialog(QDialog):
|
|||||||
def _on_timer_tick(self):
|
def _on_timer_tick(self):
|
||||||
self.countdown_seconds -= 1
|
self.countdown_seconds -= 1
|
||||||
if self.countdown_seconds > 0:
|
if self.countdown_seconds > 0:
|
||||||
self.countdown_label.setText(f"Auto-accepting in {self.countdown_seconds} seconds...")
|
self.countdown_label.setText(
|
||||||
|
f"Auto-accepting in {self.countdown_seconds} seconds..."
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
self._button_box.accepted.emit() # Simulate OK click
|
self._button_box.accepted.emit() # Simulate OK click
|
||||||
@@ -152,7 +163,7 @@ class ConversionThread(QThread):
|
|||||||
start_time,
|
start_time,
|
||||||
total_char_count,
|
total_char_count,
|
||||||
use_gpu=True,
|
use_gpu=True,
|
||||||
from_queue=False
|
from_queue=False,
|
||||||
): # Add use_gpu parameter
|
): # Add use_gpu parameter
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._chapter_options_event = threading.Event()
|
self._chapter_options_event = threading.Event()
|
||||||
@@ -182,7 +193,9 @@ class ConversionThread(QThread):
|
|||||||
self.use_gpu = use_gpu # Store the GPU setting
|
self.use_gpu = use_gpu # Store the GPU setting
|
||||||
self.max_subtitle_words = 50 # Default value, will be overridden from GUI
|
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
|
Process audio segments in memory-efficient chunks
|
||||||
|
|
||||||
@@ -204,16 +217,18 @@ class ConversionThread(QThread):
|
|||||||
for i, segment in enumerate(segments):
|
for i, segment in enumerate(segments):
|
||||||
try:
|
try:
|
||||||
# Handle both NumPy arrays and PyTorch tensors
|
# Handle both NumPy arrays and PyTorch tensors
|
||||||
if hasattr(segment, 'astype'):
|
if hasattr(segment, "astype"):
|
||||||
segment_bytes = segment.astype("float32").tobytes()
|
segment_bytes = segment.astype("float32").tobytes()
|
||||||
else:
|
else:
|
||||||
segment_bytes = segment.cpu().numpy().astype("float32").tobytes()
|
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
|
# Update progress periodically - skip if there's only one segment
|
||||||
if (i % 20 == 0 or is_last) and len(segments) > 1:
|
if (i % 20 == 0 or is_last) and len(segments) > 1:
|
||||||
progress_percent = int((samples_processed / total_samples) * 100)
|
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 this segment
|
||||||
process_func(segment_bytes, is_last)
|
process_func(segment_bytes, is_last)
|
||||||
@@ -229,7 +244,14 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
return samples_processed
|
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
|
Process audio segments to a target format with memory-efficient handling
|
||||||
|
|
||||||
@@ -254,7 +276,7 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
# Append remaining segments
|
# Append remaining segments
|
||||||
for i, segment in enumerate(audio_segments[1:], 1):
|
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.seek(0, sf.SEEK_END)
|
||||||
f.write(segment)
|
f.write(segment)
|
||||||
return True, output_path
|
return True, output_path
|
||||||
@@ -268,12 +290,18 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
# Basic FFmpeg command
|
# Basic FFmpeg command
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg", "-y",
|
"ffmpeg",
|
||||||
"-thread_queue_size", "32768",
|
"-y",
|
||||||
"-f", "f32le",
|
"-thread_queue_size",
|
||||||
"-ar", "24000",
|
"32768",
|
||||||
"-ac", "1",
|
"-f",
|
||||||
"-i", "pipe:0"
|
"f32le",
|
||||||
|
"-ar",
|
||||||
|
"24000",
|
||||||
|
"-ac",
|
||||||
|
"1",
|
||||||
|
"-i",
|
||||||
|
"pipe:0",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add custom FFmpeg arguments if provided
|
# Add custom FFmpeg arguments if provided
|
||||||
@@ -302,19 +330,29 @@ class ConversionThread(QThread):
|
|||||||
def process_chunk(chunk_bytes, is_last):
|
def process_chunk(chunk_bytes, is_last):
|
||||||
proc.stdin.write(chunk_bytes)
|
proc.stdin.write(chunk_bytes)
|
||||||
|
|
||||||
self._stream_audio_in_chunks(audio_segments, process_chunk,
|
self._stream_audio_in_chunks(
|
||||||
progress_prefix=f"Processing {output_format.upper()}")
|
audio_segments,
|
||||||
|
process_chunk,
|
||||||
|
progress_prefix=f"Processing {output_format.upper()}",
|
||||||
|
)
|
||||||
|
|
||||||
# Close stdin and wait for process to complete
|
# Close stdin and wait for process to complete
|
||||||
proc.stdin.close()
|
proc.stdin.close()
|
||||||
if proc.wait() != 0:
|
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 False, None
|
||||||
|
|
||||||
return True, output_path
|
return True, output_path
|
||||||
|
|
||||||
except Exception as e:
|
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()
|
proc.stdin.close()
|
||||||
try:
|
try:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
@@ -325,12 +363,16 @@ class ConversionThread(QThread):
|
|||||||
# For formats supported by soundfile (mp3, flac)
|
# For formats supported by soundfile (mp3, flac)
|
||||||
else:
|
else:
|
||||||
try:
|
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):
|
for i, segment in enumerate(audio_segments):
|
||||||
f.write(segment)
|
f.write(segment)
|
||||||
return True, output_path
|
return True, output_path
|
||||||
except Exception as e:
|
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
|
return False, None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@@ -346,7 +388,9 @@ class ConversionThread(QThread):
|
|||||||
if getattr(self, "from_queue", False):
|
if getattr(self, "from_queue", False):
|
||||||
display_file = self.file_name
|
display_file = self.file_name
|
||||||
else:
|
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}")
|
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"- Speed: {self.speed}")
|
||||||
self.log_updated.emit(f"- Subtitle mode: {self.subtitle_mode}")
|
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"- 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}")
|
self.log_updated.emit(f"- Save option: {self.save_option}")
|
||||||
if self.replace_single_newlines:
|
if self.replace_single_newlines:
|
||||||
self.log_updated.emit(f"- Replace single newlines: Yes")
|
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'}"
|
f"- Merge chapters at the end: {'Yes' if merge_at_end else 'No'}"
|
||||||
)
|
)
|
||||||
# Display the separate chapters format if it's set
|
# Display the separate chapters format if it's set
|
||||||
separate_format = getattr(self, 'separate_chapters_format', 'wav')
|
separate_format = getattr(self, "separate_chapters_format", "wav")
|
||||||
self.log_updated.emit(f"- Separate chapters format: {separate_format}")
|
self.log_updated.emit(
|
||||||
|
f"- Separate chapters format: {separate_format}"
|
||||||
|
)
|
||||||
|
|
||||||
if self.save_option == "Choose output folder":
|
if self.save_option == "Choose output folder":
|
||||||
self.log_updated.emit(
|
self.log_updated.emit(
|
||||||
@@ -478,7 +526,6 @@ class ConversionThread(QThread):
|
|||||||
chapters_out_dir = None
|
chapters_out_dir = None
|
||||||
suffix = ""
|
suffix = ""
|
||||||
|
|
||||||
|
|
||||||
# Use file_name for logs if from_queue, otherwise use display_path if available
|
# Use file_name for logs if from_queue, otherwise use display_path if available
|
||||||
if getattr(self, "from_queue", False):
|
if getattr(self, "from_queue", False):
|
||||||
base_path = self.file_name
|
base_path = self.file_name
|
||||||
@@ -701,11 +748,11 @@ class ConversionThread(QThread):
|
|||||||
# Find last word boundary before limit
|
# Find last word boundary before limit
|
||||||
pos = sanitized[:MAX_LEN].rfind("_")
|
pos = sanitized[:MAX_LEN].rfind("_")
|
||||||
# Use word boundary if found, otherwise use hard limit
|
# 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}"
|
chapter_filename = f"{chapter_idx:02d}_{sanitized}"
|
||||||
|
|
||||||
# Use separate_chapters_format
|
# 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(
|
chapter_out_path = os.path.join(
|
||||||
chapters_out_dir, f"{chapter_filename}.{separate_format}"
|
chapters_out_dir, f"{chapter_filename}.{separate_format}"
|
||||||
@@ -713,30 +760,49 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
# Process audio segments using the unified function
|
# Process audio segments using the unified function
|
||||||
success, chapter_out_path = self._process_audio_segments(
|
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:
|
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
|
continue
|
||||||
|
|
||||||
# Generate subtitle file for chapter if not Disabled
|
# Generate subtitle file for chapter if not Disabled
|
||||||
if self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
if self.subtitle_mode != "Disabled" and chapter_subtitle_entries:
|
||||||
subtitle_format = getattr(self, 'subtitle_format', 'srt')
|
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||||
file_extension = 'ass' if 'ass' in subtitle_format else 'srt'
|
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||||
chapter_subtitle_path = os.path.join(
|
chapter_subtitle_path = os.path.join(
|
||||||
chapters_out_dir, f"{chapter_filename}.{file_extension}"
|
chapters_out_dir, f"{chapter_filename}.{file_extension}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'ass' in subtitle_format:
|
if "ass" in subtitle_format:
|
||||||
# Generate ASS subtitle
|
# Generate ASS subtitle
|
||||||
is_centered = subtitle_format in ("ass_centered_wide", "ass_centered_narrow")
|
is_centered = subtitle_format in (
|
||||||
is_narrow = subtitle_format in ("ass_narrow", "ass_centered_narrow")
|
"ass_centered_wide",
|
||||||
self._write_ass_subtitle(chapter_subtitle_path, chapter_subtitle_entries, is_centered, is_narrow)
|
"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:
|
else:
|
||||||
# Generate SRT subtitle (default)
|
# Generate SRT subtitle (default)
|
||||||
with open(
|
with open(
|
||||||
chapter_subtitle_path, "w", encoding="utf-8", errors="replace"
|
chapter_subtitle_path,
|
||||||
|
"w",
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
) as srt_file:
|
) as srt_file:
|
||||||
for i, (start, end, text) in enumerate(
|
for i, (start, end, text) in enumerate(
|
||||||
chapter_subtitle_entries, 1
|
chapter_subtitle_entries, 1
|
||||||
@@ -788,9 +854,10 @@ class ConversionThread(QThread):
|
|||||||
else:
|
else:
|
||||||
# Process audio segments using the unified function
|
# Process audio segments using the unified function
|
||||||
success, final_out_path = self._process_audio_segments(
|
success, final_out_path = self._process_audio_segments(
|
||||||
audio_segments, f"{base_filepath_no_ext}.{intended_output_format}",
|
audio_segments,
|
||||||
|
f"{base_filepath_no_ext}.{intended_output_format}",
|
||||||
intended_output_format,
|
intended_output_format,
|
||||||
use_ffmpeg=(intended_output_format in ["opus", "m4b"])
|
use_ffmpeg=(intended_output_format in ["opus", "m4b"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
@@ -798,25 +865,41 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
if not final_out_path:
|
if not final_out_path:
|
||||||
self.log_updated.emit(("Audio generation failed.", "red"))
|
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
|
return
|
||||||
|
|
||||||
# Subtitle and final message logic
|
# Subtitle and final message logic
|
||||||
if final_out_path:
|
if final_out_path:
|
||||||
if self.subtitle_mode != "Disabled":
|
if self.subtitle_mode != "Disabled":
|
||||||
subtitle_format = getattr(self, 'subtitle_format', 'srt')
|
subtitle_format = getattr(self, "subtitle_format", "srt")
|
||||||
file_extension = 'ass' if 'ass' in subtitle_format else 'srt'
|
file_extension = "ass" if "ass" in subtitle_format else "srt"
|
||||||
subtitle_path = os.path.splitext(final_out_path)[0] + f".{file_extension}"
|
subtitle_path = (
|
||||||
|
os.path.splitext(final_out_path)[0] + f".{file_extension}"
|
||||||
|
)
|
||||||
|
|
||||||
if 'ass' in subtitle_format:
|
if "ass" in subtitle_format:
|
||||||
# Generate ASS subtitle
|
# Generate ASS subtitle
|
||||||
is_centered = subtitle_format in ("ass_centered_wide", "ass_centered_narrow")
|
is_centered = subtitle_format in (
|
||||||
is_narrow = subtitle_format in ("ass_narrow", "ass_centered_narrow")
|
"ass_centered_wide",
|
||||||
self._write_ass_subtitle(subtitle_path, subtitle_entries, is_centered, is_narrow)
|
"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:
|
else:
|
||||||
# Generate SRT subtitle (default)
|
# Generate SRT subtitle (default)
|
||||||
with open(subtitle_path, "w", encoding="utf-8", errors="replace") as srt_file:
|
with open(
|
||||||
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
subtitle_path, "w", encoding="utf-8", errors="replace"
|
||||||
|
) as srt_file:
|
||||||
|
for i, (start, end, text) in enumerate(
|
||||||
|
subtitle_entries, 1
|
||||||
|
):
|
||||||
srt_file.write(
|
srt_file.write(
|
||||||
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
f"{i}\n{self._srt_time(start)} --> {self._srt_time(end)}\n{text}\n\n"
|
||||||
)
|
)
|
||||||
@@ -829,11 +912,16 @@ class ConversionThread(QThread):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.conversion_finished.emit(
|
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:
|
else:
|
||||||
self.log_updated.emit(("Audio generation failed (final_out_path was not set).", "red"))
|
self.log_updated.emit(
|
||||||
self.conversion_finished.emit(("Audio generation failed.", "red"), None)
|
("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:
|
elif audio_segments and not merge_chapters:
|
||||||
self.conversion_finished.emit(
|
self.conversion_finished.emit(
|
||||||
(
|
(
|
||||||
@@ -856,7 +944,9 @@ class ConversionThread(QThread):
|
|||||||
self.waiting_for_user_input = False
|
self.waiting_for_user_input = False
|
||||||
self._chapter_options_event.set()
|
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"""
|
"""Generate M4B file with chapters from audio segments"""
|
||||||
final_wav_path = f"{base_filepath_no_ext}.wav"
|
final_wav_path = f"{base_filepath_no_ext}.wav"
|
||||||
output_m4b_path = f"{base_filepath_no_ext}.m4b"
|
output_m4b_path = f"{base_filepath_no_ext}.m4b"
|
||||||
@@ -877,7 +967,9 @@ class ConversionThread(QThread):
|
|||||||
if success:
|
if success:
|
||||||
return wav_path
|
return wav_path
|
||||||
else:
|
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
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -885,7 +977,7 @@ class ConversionThread(QThread):
|
|||||||
with open(chapters_info_path, "w", encoding="utf-8") as f:
|
with open(chapters_info_path, "w", encoding="utf-8") as f:
|
||||||
f.write(";FFMETADATA1\n")
|
f.write(";FFMETADATA1\n")
|
||||||
for chapter in chapters_time:
|
for chapter in chapters_time:
|
||||||
chapter_title = chapter['chapter'].replace('=', '\\=')
|
chapter_title = chapter["chapter"].replace("=", "\\=")
|
||||||
f.write(f"[CHAPTER]\n")
|
f.write(f"[CHAPTER]\n")
|
||||||
f.write(f"TIMEBASE=1/1000\n")
|
f.write(f"TIMEBASE=1/1000\n")
|
||||||
f.write(f"START={int(chapter['start']*1000)}\n")
|
f.write(f"START={int(chapter['start']*1000)}\n")
|
||||||
@@ -898,19 +990,30 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
# Use pipe-based approach for audio input with special args for M4B chapters
|
# Use pipe-based approach for audio input with special args for M4B chapters
|
||||||
ffmpeg_args = [
|
ffmpeg_args = [
|
||||||
"-i", chapters_info_path,
|
"-i",
|
||||||
"-map", "0:a",
|
chapters_info_path,
|
||||||
"-map_metadata", "1",
|
"-map",
|
||||||
"-map_chapters", "1",
|
"0:a",
|
||||||
|
"-map_metadata",
|
||||||
|
"1",
|
||||||
|
"-map_chapters",
|
||||||
|
"1",
|
||||||
*metadata_options,
|
*metadata_options,
|
||||||
"-c:a", "aac",
|
"-c:a",
|
||||||
"-q:a", "2", # Quality-based VBR for better quality control
|
"aac",
|
||||||
"-movflags", "+faststart+use_metadata_tags", # Added for better compatibility
|
"-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
|
# Use the established unified method with M4B-specific args
|
||||||
success, out_path = self._process_audio_segments(
|
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
|
# Clean up the temporary chapter metadata file
|
||||||
@@ -918,13 +1021,17 @@ class ConversionThread(QThread):
|
|||||||
try:
|
try:
|
||||||
os.remove(chapters_info_path)
|
os.remove(chapters_info_path)
|
||||||
except Exception as e:
|
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:
|
if success:
|
||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
# If M4B generation failed, fallback to WAV
|
# 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(
|
success, wav_path = self._process_audio_segments(
|
||||||
audio_segments, final_wav_path, "wav", use_ffmpeg=False
|
audio_segments, final_wav_path, "wav", use_ffmpeg=False
|
||||||
)
|
)
|
||||||
@@ -936,7 +1043,12 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# General error during M4B generation - create WAV file directly as final output
|
# 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
|
# Use the established unified method to create a WAV file as fallback
|
||||||
success, wav_path = self._process_audio_segments(
|
success, wav_path = self._process_audio_segments(
|
||||||
@@ -953,7 +1065,9 @@ class ConversionThread(QThread):
|
|||||||
if success:
|
if success:
|
||||||
return wav_path
|
return wav_path
|
||||||
else:
|
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
|
return None
|
||||||
|
|
||||||
def _extract_and_add_metadata_tags_to_ffmpeg_cmd(self):
|
def _extract_and_add_metadata_tags_to_ffmpeg_cmd(self):
|
||||||
@@ -967,10 +1081,14 @@ class ConversionThread(QThread):
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
encoding = detect_encoding(self.file_name)
|
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()
|
text = file.read()
|
||||||
except Exception as e:
|
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 []
|
return []
|
||||||
|
|
||||||
# Extract metadata tags using regex
|
# Extract metadata tags using regex
|
||||||
@@ -988,7 +1106,11 @@ class ConversionThread(QThread):
|
|||||||
if getattr(self, "from_queue", False):
|
if getattr(self, "from_queue", False):
|
||||||
filename = os.path.splitext(os.path.basename(self.file_name))[0]
|
filename = os.path.splitext(os.path.basename(self.file_name))[0]
|
||||||
else:
|
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:
|
if title_match:
|
||||||
metadata_options.extend(["-metadata", f"title={title_match.group(1)}"])
|
metadata_options.extend(["-metadata", f"title={title_match.group(1)}"])
|
||||||
@@ -1013,18 +1135,23 @@ class ConversionThread(QThread):
|
|||||||
else:
|
else:
|
||||||
# Use current year if year is not specified
|
# Use current year if year is not specified
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
current_year = datetime.datetime.now().year
|
current_year = datetime.datetime.now().year
|
||||||
metadata_options.extend(["-metadata", f"date={current_year}"])
|
metadata_options.extend(["-metadata", f"date={current_year}"])
|
||||||
|
|
||||||
# Add album artist metadata
|
# Add album artist metadata
|
||||||
if album_artist_match:
|
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:
|
else:
|
||||||
metadata_options.extend(["-metadata", f"album_artist=Unknown"])
|
metadata_options.extend(["-metadata", f"album_artist=Unknown"])
|
||||||
|
|
||||||
# Add composer metadata
|
# Add composer metadata
|
||||||
if composer_match:
|
if composer_match:
|
||||||
metadata_options.extend(["-metadata", f"composer={composer_match.group(1)}"])
|
metadata_options.extend(
|
||||||
|
["-metadata", f"composer={composer_match.group(1)}"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
metadata_options.extend(["-metadata", f"composer=Narrator"])
|
metadata_options.extend(["-metadata", f"composer=Narrator"])
|
||||||
|
|
||||||
@@ -1139,7 +1266,9 @@ class ConversionThread(QThread):
|
|||||||
(group[0]["start"], group[-1]["end"], text.strip())
|
(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:
|
with open(file_path, "w", encoding="utf-8", errors="replace") as f:
|
||||||
# Minimal ASS header
|
# Minimal ASS header
|
||||||
f.write("[Script Info]\n")
|
f.write("[Script Info]\n")
|
||||||
@@ -1148,7 +1277,9 @@ class ConversionThread(QThread):
|
|||||||
|
|
||||||
# Only events section, use override tags for positioning
|
# Only events section, use override tags for positioning
|
||||||
f.write("[Events]\n")
|
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
|
# Set margin based on is_narrow parameter
|
||||||
margin = "90" if is_narrow else ""
|
margin = "90" if is_narrow else ""
|
||||||
@@ -1161,7 +1292,9 @@ class ConversionThread(QThread):
|
|||||||
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
for i, (start, end, text) in enumerate(subtitle_entries, 1):
|
||||||
start_time = self._ass_time(start)
|
start_time = self._ass_time(start)
|
||||||
end_time = self._ass_time(end)
|
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):
|
def cancel(self):
|
||||||
self.cancel_requested = True
|
self.cancel_requested = True
|
||||||
@@ -1198,7 +1331,9 @@ class VoicePreviewThread(QThread):
|
|||||||
self.use_gpu = use_gpu
|
self.use_gpu = use_gpu
|
||||||
|
|
||||||
# Cache location for preview audio
|
# 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)
|
os.makedirs(self.cache_dir, exist_ok=True)
|
||||||
|
|
||||||
# Calculate cache path
|
# Calculate cache path
|
||||||
@@ -1208,7 +1343,9 @@ class VoicePreviewThread(QThread):
|
|||||||
"""Generate a unique filename for the voice with its parameters"""
|
"""Generate a unique filename for the voice with its parameters"""
|
||||||
# For a voice formula, use a hash of the formula
|
# For a voice formula, use a hash of the formula
|
||||||
if "*" in self.voice:
|
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:
|
else:
|
||||||
voice_id = self.voice
|
voice_id = self.voice
|
||||||
|
|
||||||
@@ -1282,7 +1419,9 @@ class PlayAudioThread(QThread):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle initialization errors separately to give better error messages
|
# Handle initialization errors separately to give better error messages
|
||||||
if "mixer not initialized" in str(e):
|
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:
|
else:
|
||||||
self.error.emit(f"Audio playback error: {str(e)}")
|
self.error.emit(f"Audio playback error: {str(e)}")
|
||||||
|
|
||||||
@@ -1292,6 +1431,7 @@ class PlayAudioThread(QThread):
|
|||||||
# Try to stop pygame if it's running, but catch all exceptions
|
# Try to stop pygame if it's running, but catch all exceptions
|
||||||
try:
|
try:
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
if pygame.mixer.get_init():
|
if pygame.mixer.get_init():
|
||||||
if pygame.mixer.music.get_busy():
|
if pygame.mixer.music.get_busy():
|
||||||
pygame.mixer.music.stop()
|
pygame.mixer.music.stop()
|
||||||
|
|||||||
+302
-115
@@ -74,7 +74,7 @@ from abogen.constants import (
|
|||||||
LANGUAGE_DESCRIPTIONS,
|
LANGUAGE_DESCRIPTIONS,
|
||||||
VOICES_INTERNAL,
|
VOICES_INTERNAL,
|
||||||
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
COLORS
|
COLORS,
|
||||||
)
|
)
|
||||||
import threading
|
import threading
|
||||||
from abogen.voice_formula_gui import VoiceFormulaDialog
|
from abogen.voice_formula_gui import VoiceFormulaDialog
|
||||||
@@ -85,6 +85,7 @@ if platform.system() == "Windows":
|
|||||||
import ctypes
|
import ctypes
|
||||||
import winreg
|
import winreg
|
||||||
|
|
||||||
|
|
||||||
class DarkTitleBarEventFilter(QObject):
|
class DarkTitleBarEventFilter(QObject):
|
||||||
def __init__(self, is_windows, get_dark_mode_func, set_title_bar_dark_mode_func):
|
def __init__(self, is_windows, get_dark_mode_func, set_title_bar_dark_mode_func):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -100,6 +101,7 @@ class DarkTitleBarEventFilter(QObject):
|
|||||||
self.set_title_bar_dark_mode(obj, True)
|
self.set_title_bar_dark_mode(obj, True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class ShowWarningSignalEmitter(QObject): # New class to handle signal emission
|
class ShowWarningSignalEmitter(QObject): # New class to handle signal emission
|
||||||
show_warning_signal = pyqtSignal(str, str)
|
show_warning_signal = pyqtSignal(str, str)
|
||||||
|
|
||||||
@@ -128,10 +130,14 @@ class InputBox(QLabel):
|
|||||||
STYLE_DEFAULT_HOVER = f"background:{COLORS['BLUE_BG_HOVER']}; border-color:{COLORS['BLUE_BORDER_HOVER']};"
|
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 = 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 = 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):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -140,7 +146,9 @@ class InputBox(QLabel):
|
|||||||
self.setText(
|
self.setText(
|
||||||
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf)"
|
"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.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
||||||
@@ -172,7 +180,9 @@ class InputBox(QLabel):
|
|||||||
# Add Go to folder button
|
# Add Go to folder button
|
||||||
self.go_to_folder_btn = QPushButton("Go to folder", self)
|
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.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.clicked.connect(self.on_go_to_folder_clicked)
|
||||||
self.go_to_folder_btn.hide()
|
self.go_to_folder_btn.hide()
|
||||||
|
|
||||||
@@ -215,7 +225,6 @@ class InputBox(QLabel):
|
|||||||
return float(match.group(1))
|
return float(match.group(1))
|
||||||
raise ValueError(f"Invalid size format: {size_str}")
|
raise ValueError(f"Invalid size format: {size_str}")
|
||||||
|
|
||||||
|
|
||||||
# Format numbers with commas
|
# Format numbers with commas
|
||||||
def format_num(n):
|
def format_num(n):
|
||||||
try:
|
try:
|
||||||
@@ -265,7 +274,9 @@ class InputBox(QLabel):
|
|||||||
)
|
)
|
||||||
# Set fixed width to force wrapping
|
# Set fixed width to force wrapping
|
||||||
self.setWordWrap(True)
|
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()
|
self.clear_btn.show()
|
||||||
is_document = self.window().selected_file_type in ["epub", "pdf"]
|
is_document = self.window().selected_file_type in ["epub", "pdf"]
|
||||||
self.chapters_btn.setVisible(is_document)
|
self.chapters_btn.setVisible(is_document)
|
||||||
@@ -278,7 +289,6 @@ class InputBox(QLabel):
|
|||||||
else: # PDF - always use Pages
|
else: # PDF - always use Pages
|
||||||
self.chapters_btn.setText(f"Pages ({chapter_count})")
|
self.chapters_btn.setText(f"Pages ({chapter_count})")
|
||||||
|
|
||||||
|
|
||||||
# Hide textbox and show edit only for .txt files
|
# Hide textbox and show edit only for .txt files
|
||||||
self.textbox_btn.hide()
|
self.textbox_btn.hide()
|
||||||
# Show edit button for txt files directly
|
# Show edit button for txt files directly
|
||||||
@@ -296,21 +306,23 @@ class InputBox(QLabel):
|
|||||||
self.go_to_folder_btn.show()
|
self.go_to_folder_btn.show()
|
||||||
# Enable add to queue button only when file is accepted (input box is green)
|
# Enable add to queue button only when file is accepted (input box is green)
|
||||||
self.resizeEvent(None)
|
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.window().btn_add_to_queue.setEnabled(True)
|
||||||
|
|
||||||
self.chapters_btn.adjustSize()
|
self.chapters_btn.adjustSize()
|
||||||
# Reset the input_box_cleared_by_queue flag after setting file info
|
# 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
|
self.window().input_box_cleared_by_queue = False
|
||||||
|
|
||||||
def set_error(self, message):
|
def set_error(self, message):
|
||||||
self.setText(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
|
# Show textbox button in error state as well
|
||||||
self.textbox_btn.show()
|
self.textbox_btn.show()
|
||||||
# Disable add to queue button on error
|
# 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)
|
self.window().btn_add_to_queue.setEnabled(False)
|
||||||
|
|
||||||
def clear_input(self):
|
def clear_input(self):
|
||||||
@@ -321,7 +333,9 @@ class InputBox(QLabel):
|
|||||||
self.setText(
|
self.setText(
|
||||||
"Drag and drop your file here or click to browse.\n(.txt, .epub, .pdf)"
|
"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.clear_btn.hide()
|
||||||
self.chapters_btn.hide()
|
self.chapters_btn.hide()
|
||||||
self.chapters_btn.setText("Chapters") # Reset text
|
self.chapters_btn.setText("Chapters") # Reset text
|
||||||
@@ -330,10 +344,10 @@ class InputBox(QLabel):
|
|||||||
self.edit_btn.hide()
|
self.edit_btn.hide()
|
||||||
self.go_to_folder_btn.hide()
|
self.go_to_folder_btn.hide()
|
||||||
# Disable add to queue button when input is cleared
|
# 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)
|
self.window().btn_add_to_queue.setEnabled(False)
|
||||||
# Reset the input_box_cleared_by_queue flag after setting file info
|
# 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
|
self.window().input_box_cleared_by_queue = True
|
||||||
|
|
||||||
def _human_readable_size(self, size, decimal_places=2):
|
def _human_readable_size(self, size, decimal_places=2):
|
||||||
@@ -352,26 +366,42 @@ class InputBox(QLabel):
|
|||||||
urls = event.mimeData().urls()
|
urls = event.mimeData().urls()
|
||||||
if urls:
|
if urls:
|
||||||
ext = urls[0].toLocalFile().lower()
|
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()
|
event.acceptProposedAction()
|
||||||
# Set hover style based on current state
|
# Set hover style based on current state
|
||||||
if self.styleSheet().find(self.STYLE_ACTIVE) != -1:
|
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:
|
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:
|
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
|
return
|
||||||
event.ignore()
|
event.ignore()
|
||||||
|
|
||||||
def dragLeaveEvent(self, event):
|
def dragLeaveEvent(self, event):
|
||||||
# Restore the style based on current state
|
# Restore the style based on current state
|
||||||
if self.styleSheet().find(self.STYLE_ACTIVE) != -1:
|
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:
|
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:
|
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()
|
event.accept()
|
||||||
|
|
||||||
def dropEvent(self, event):
|
def dropEvent(self, event):
|
||||||
@@ -433,7 +463,11 @@ class InputBox(QLabel):
|
|||||||
# win.selected_file holds the path to the text that is converted.
|
# win.selected_file holds the path to the text that is converted.
|
||||||
file_to_check = win.selected_file
|
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)
|
folder_path = os.path.dirname(file_to_check)
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
|
||||||
else:
|
else:
|
||||||
@@ -582,6 +616,7 @@ class TextboxDialog(QDialog):
|
|||||||
self.update_char_count()
|
self.update_char_count()
|
||||||
self.text_edit.setFocus()
|
self.text_edit.setFocus()
|
||||||
|
|
||||||
|
|
||||||
def migrate_subtitle_format(config):
|
def migrate_subtitle_format(config):
|
||||||
"""Convert old subtitle_format values to new internal keys."""
|
"""Convert old subtitle_format values to new internal keys."""
|
||||||
old_to_new = {
|
old_to_new = {
|
||||||
@@ -596,6 +631,7 @@ def migrate_subtitle_format(config):
|
|||||||
config["subtitle_format"] = old_to_new[val]
|
config["subtitle_format"] = old_to_new[val]
|
||||||
save_config(config)
|
save_config(config)
|
||||||
|
|
||||||
|
|
||||||
class abogen(QWidget):
|
class abogen(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -631,7 +667,9 @@ class abogen(QWidget):
|
|||||||
"max_subtitle_words", 50
|
"max_subtitle_words", 50
|
||||||
) # Default max words per subtitle
|
) # Default max words per subtitle
|
||||||
self.selected_format = self.config.get("selected_format", "wav")
|
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(
|
self.use_gpu = self.config.get(
|
||||||
"use_gpu", True # Load GPU setting with default True
|
"use_gpu", True # Load GPU setting with default True
|
||||||
)
|
)
|
||||||
@@ -645,7 +683,9 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
# Create warning signal emitter
|
# Create warning signal emitter
|
||||||
self.warning_signal_emitter = ShowWarningSignalEmitter()
|
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)
|
hf_tracker.set_show_warning_signal_emitter(self.warning_signal_emitter)
|
||||||
|
|
||||||
# Set application icon
|
# 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.setStyleSheet("QPushButton { padding: 6px 12px; }")
|
||||||
self.btn_voice_formula_mixer.clicked.connect(self.show_voice_formula_dialog)
|
self.btn_voice_formula_mixer.clicked.connect(self.show_voice_formula_dialog)
|
||||||
voice_layout.addWidget(self.btn_voice_formula_mixer)
|
voice_layout.addWidget(self.btn_voice_formula_mixer)
|
||||||
|
|
||||||
# Play/Stop icons
|
# Play/Stop icons
|
||||||
def make_icon(color, shape):
|
def make_icon(color, shape):
|
||||||
pix = QPixmap(20, 20)
|
pix = QPixmap(20, 20)
|
||||||
@@ -900,13 +941,15 @@ class abogen(QWidget):
|
|||||||
self.subtitle_format_combo.setStyleSheet(
|
self.subtitle_format_combo.setStyleSheet(
|
||||||
"QComboBox { min-height: 20px; padding: 6px 12px; }"
|
"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 = [
|
subtitle_formats = [
|
||||||
("srt", "SRT (standard)"),
|
("srt", "SRT (standard)"),
|
||||||
("ass_wide", "ASS (wide)"),
|
("ass_wide", "ASS (wide)"),
|
||||||
("ass_narrow", "ASS (narrow)"),
|
("ass_narrow", "ASS (narrow)"),
|
||||||
("ass_centered_wide", "ASS (centered wide)"),
|
("ass_centered_wide", "ASS (centered wide)"),
|
||||||
("ass_centered_narrow", "ASS (centered narrow)")
|
("ass_centered_narrow", "ASS (centered narrow)"),
|
||||||
]
|
]
|
||||||
for value, text in subtitle_formats:
|
for value, text in subtitle_formats:
|
||||||
self.subtitle_format_combo.addItem(text, value)
|
self.subtitle_format_combo.addItem(text, value)
|
||||||
@@ -927,13 +970,19 @@ class abogen(QWidget):
|
|||||||
replace_newlines_layout.addWidget(replace_newlines_label)
|
replace_newlines_layout.addWidget(replace_newlines_label)
|
||||||
self.replace_newlines_combo = QComboBox(self)
|
self.replace_newlines_combo = QComboBox(self)
|
||||||
self.replace_newlines_combo.addItems(["Disabled", "Enabled"])
|
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(
|
self.replace_newlines_combo.setStyleSheet(
|
||||||
"QComboBox { min-height: 20px; padding: 6px 12px; }"
|
"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
|
# 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(
|
self.replace_newlines_combo.currentIndexChanged.connect(
|
||||||
lambda idx: self.toggle_replace_single_newlines(idx == 1)
|
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)
|
selected_folder_label = QLabel("Selected folder:", self.save_path_row_widget)
|
||||||
save_path_row.addWidget(selected_folder_label)
|
save_path_row.addWidget(selected_folder_label)
|
||||||
self.save_path_label = QLabel("", self.save_path_row_widget)
|
self.save_path_label = QLabel("", self.save_path_row_widget)
|
||||||
self.save_path_label.setStyleSheet(
|
self.save_path_label.setStyleSheet(f"QLabel {{ color: {COLORS['GREEN']}; }}")
|
||||||
f"QLabel {{ color: {COLORS['GREEN']}; }}"
|
|
||||||
)
|
|
||||||
self.save_path_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
self.save_path_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||||
save_path_row.addWidget(self.save_path_label)
|
save_path_row.addWidget(self.save_path_label)
|
||||||
self.save_path_row_widget.hide() # Hide the whole row by default
|
self.save_path_row_widget.hide() # Hide the whole row by default
|
||||||
@@ -1119,6 +1166,7 @@ class abogen(QWidget):
|
|||||||
dialog.setWindowModality(Qt.NonModal)
|
dialog.setWindowModality(Qt.NonModal)
|
||||||
dialog.setModal(False)
|
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):
|
def on_dialog_finished(result):
|
||||||
if result != QDialog.Accepted:
|
if result != QDialog.Accepted:
|
||||||
return False
|
return False
|
||||||
@@ -1161,22 +1209,49 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
# Save metadata if available
|
# Save metadata if available
|
||||||
meta_dir = os.path.join(project_dir, "metadata")
|
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"):
|
if hasattr(dialog, "book_metadata"):
|
||||||
meta_path = os.path.join(meta_dir, "book_info.txt")
|
meta_path = os.path.join(meta_dir, "book_info.txt")
|
||||||
with open(meta_path, "w", encoding="utf-8") as f:
|
with open(meta_path, "w", encoding="utf-8") as f:
|
||||||
# Clean HTML tags from metadata
|
# Clean HTML tags from metadata
|
||||||
title = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('title', 'Unknown')))
|
title = re.sub(
|
||||||
publisher = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('publisher', 'Unknown')))
|
r"<[^>]+>",
|
||||||
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')))
|
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"Title: {title}\n")
|
||||||
f.write(f"Authors: {', '.join(authors)}\n")
|
f.write(f"Authors: {', '.join(authors)}\n")
|
||||||
f.write(f"Publisher: {publisher}\n")
|
f.write(f"Publisher: {publisher}\n")
|
||||||
f.write(f"Publication Year: {publication_year}\n")
|
f.write(f"Publication Year: {publication_year}\n")
|
||||||
if dialog.book_metadata.get('description'):
|
if dialog.book_metadata.get("description"):
|
||||||
description = re.sub(r'<[^>]+>', '', str(dialog.book_metadata.get('description')))
|
description = re.sub(
|
||||||
|
r"<[^>]+>",
|
||||||
|
"",
|
||||||
|
str(dialog.book_metadata.get("description")),
|
||||||
|
)
|
||||||
f.write(f"\nDescription:\n{description}\n")
|
f.write(f"\nDescription:\n{description}\n")
|
||||||
|
|
||||||
# Save cover image if available
|
# Save cover image if available
|
||||||
@@ -1404,7 +1479,7 @@ class abogen(QWidget):
|
|||||||
self._update_log_main_thread(message)
|
self._update_log_main_thread(message)
|
||||||
|
|
||||||
def _update_log_main_thread(self, message):
|
def _update_log_main_thread(self, message):
|
||||||
if not hasattr(self, '_log_lines'):
|
if not hasattr(self, "_log_lines"):
|
||||||
self._log_lines = []
|
self._log_lines = []
|
||||||
|
|
||||||
# Always produce a single HTML line (with color if tuple)
|
# Always produce a single HTML line (with color if tuple)
|
||||||
@@ -1423,7 +1498,7 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
# Trim buffer if needed
|
# Trim buffer if needed
|
||||||
if len(self._log_lines) > self.log_window_max_lines:
|
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()
|
self.log_text.clear()
|
||||||
for line in self._log_lines:
|
for line in self._log_lines:
|
||||||
self.log_text.insertHtml(line)
|
self.log_text.insertHtml(line)
|
||||||
@@ -1438,7 +1513,11 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
def _get_queue_progress_format(self, value=None):
|
def _get_queue_progress_format(self, value=None):
|
||||||
"""Return the progress bar format string for queue mode."""
|
"""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
|
N = self.current_queue_index + 1
|
||||||
M = len(self.queued_items)
|
M = len(self.queued_items)
|
||||||
percent = value if value is not None else self.progress_bar.value()
|
percent = value if value is not None else self.progress_bar.value()
|
||||||
@@ -1453,7 +1532,11 @@ class abogen(QWidget):
|
|||||||
value = 99
|
value = 99
|
||||||
self.progress_bar.setValue(value)
|
self.progress_bar.setValue(value)
|
||||||
# Show queue progress if in queue mode
|
# 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
|
N = self.current_queue_index + 1
|
||||||
M = len(self.queued_items)
|
M = len(self.queued_items)
|
||||||
self.progress_bar.setFormat(f"{value}% ({N}/{M})")
|
self.progress_bar.setFormat(f"{value}% ({N}/{M})")
|
||||||
@@ -1499,9 +1582,9 @@ class abogen(QWidget):
|
|||||||
pass
|
pass
|
||||||
self.btn_start.clicked.connect(self.start_conversion)
|
self.btn_start.clicked.connect(self.start_conversion)
|
||||||
|
|
||||||
def enqueue(self, item : QueuedItem):
|
def enqueue(self, item: QueuedItem):
|
||||||
self.queued_items.append(item)
|
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
|
# enable start queue button, manage queue button
|
||||||
self.enable_disable_queue_buttons()
|
self.enable_disable_queue_buttons()
|
||||||
|
|
||||||
@@ -1509,8 +1592,15 @@ class abogen(QWidget):
|
|||||||
return self.queued_items
|
return self.queued_items
|
||||||
|
|
||||||
def add_to_queue(self):
|
def add_to_queue(self):
|
||||||
# Use the file currently displayed in the input box
|
# For epub/pdf, always use the converted txt file (selected_file)
|
||||||
file_to_queue = self.displayed_file_path if self.displayed_file_path else self.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:
|
if not file_to_queue:
|
||||||
self.input_box.set_error("Please add a file.")
|
self.input_box.set_error("Please add a file.")
|
||||||
@@ -1529,23 +1619,26 @@ class abogen(QWidget):
|
|||||||
subtitle_mode=actual_subtitle_mode,
|
subtitle_mode=actual_subtitle_mode,
|
||||||
output_format=self.selected_format,
|
output_format=self.selected_format,
|
||||||
total_char_count=self.char_count,
|
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
|
# Prevent adding duplicate items to the queue
|
||||||
for queued_item in self.queued_items:
|
for queued_item in self.queued_items:
|
||||||
if (
|
if (
|
||||||
queued_item.file_name == item_queue.file_name and
|
queued_item.file_name == item_queue.file_name
|
||||||
queued_item.lang_code == item_queue.lang_code and
|
and queued_item.lang_code == item_queue.lang_code
|
||||||
queued_item.speed == item_queue.speed and
|
and queued_item.speed == item_queue.speed
|
||||||
queued_item.voice == item_queue.voice and
|
and queued_item.voice == item_queue.voice
|
||||||
queued_item.save_option == item_queue.save_option and
|
and queued_item.save_option == item_queue.save_option
|
||||||
queued_item.output_folder == item_queue.output_folder and
|
and queued_item.output_folder == item_queue.output_folder
|
||||||
queued_item.subtitle_mode == item_queue.subtitle_mode and
|
and queued_item.subtitle_mode == item_queue.subtitle_mode
|
||||||
queued_item.output_format == item_queue.output_format and
|
and queued_item.output_format == item_queue.output_format
|
||||||
getattr(queued_item, "replace_single_newlines", False) == item_queue.replace_single_newlines
|
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
|
return
|
||||||
|
|
||||||
self.enqueue(item_queue)
|
self.enqueue(item_queue)
|
||||||
@@ -1562,7 +1655,7 @@ class abogen(QWidget):
|
|||||||
"Confirm Clear Queue",
|
"Confirm Clear Queue",
|
||||||
f"Are you sure you want to clear {len(self.queued_items)} items from the queue?",
|
f"Are you sure you want to clear {len(self.queued_items)} items from the queue?",
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
QMessageBox.No
|
QMessageBox.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.Yes:
|
||||||
return
|
return
|
||||||
@@ -1598,7 +1691,9 @@ class abogen(QWidget):
|
|||||||
self.subtitle_mode = queued_item.subtitle_mode
|
self.subtitle_mode = queued_item.subtitle_mode
|
||||||
self.selected_format = queued_item.output_format
|
self.selected_format = queued_item.output_format
|
||||||
self.char_count = queued_item.total_char_count
|
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)
|
self.start_conversion(from_queue=True)
|
||||||
else:
|
else:
|
||||||
# Queue finished, reset index
|
# Queue finished, reset index
|
||||||
@@ -1612,7 +1707,6 @@ class abogen(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.current_queue_index = 0 # Reset for next time
|
self.current_queue_index = 0 # Reset for next time
|
||||||
|
|
||||||
|
|
||||||
def get_voice_formula(self) -> str:
|
def get_voice_formula(self) -> str:
|
||||||
if self.mixed_voice_state:
|
if self.mixed_voice_state:
|
||||||
formula_components = [
|
formula_components = [
|
||||||
@@ -1625,6 +1719,7 @@ class abogen(QWidget):
|
|||||||
def get_selected_lang(self, voice_formula) -> str:
|
def get_selected_lang(self, voice_formula) -> str:
|
||||||
if self.selected_profile_name:
|
if self.selected_profile_name:
|
||||||
from abogen.voice_profiles import load_profiles
|
from abogen.voice_profiles import load_profiles
|
||||||
|
|
||||||
entry = load_profiles().get(self.selected_profile_name, {})
|
entry = load_profiles().get(self.selected_profile_name, {})
|
||||||
selected_lang = entry.get("language")
|
selected_lang = entry.get("language")
|
||||||
else:
|
else:
|
||||||
@@ -1636,11 +1731,7 @@ class abogen(QWidget):
|
|||||||
return selected_lang
|
return selected_lang
|
||||||
|
|
||||||
def get_actual_subtitle_mode(self) -> str:
|
def get_actual_subtitle_mode(self) -> str:
|
||||||
return (
|
return "Disabled" if not self.subtitle_combo.isEnabled() else self.subtitle_mode
|
||||||
"Disabled"
|
|
||||||
if not self.subtitle_combo.isEnabled()
|
|
||||||
else self.subtitle_mode
|
|
||||||
)
|
|
||||||
|
|
||||||
def start_conversion(self, from_queue=False):
|
def start_conversion(self, from_queue=False):
|
||||||
if not self.selected_file:
|
if not self.selected_file:
|
||||||
@@ -1651,7 +1742,12 @@ class abogen(QWidget):
|
|||||||
self.convert_input_box_to_log()
|
self.convert_input_box_to_log()
|
||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
# Show queue progress if in queue mode
|
# 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
|
N = self.current_queue_index + 1
|
||||||
M = len(self.queued_items)
|
M = len(self.queued_items)
|
||||||
self.progress_bar.setFormat(f"0% ({N}/{M})")
|
self.progress_bar.setFormat(f"0% ({N}/{M})")
|
||||||
@@ -1725,9 +1821,13 @@ class abogen(QWidget):
|
|||||||
self.replace_single_newlines
|
self.replace_single_newlines
|
||||||
)
|
)
|
||||||
# Pass separate_chapters_format setting
|
# 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
|
# 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
|
# Pass chapter count for EPUB or PDF files
|
||||||
if self.selected_file_type in ["epub", "pdf"] and hasattr(
|
if self.selected_file_type in ["epub", "pdf"] and hasattr(
|
||||||
self, "selected_chapters"
|
self, "selected_chapters"
|
||||||
@@ -1781,7 +1881,6 @@ class abogen(QWidget):
|
|||||||
f"Processed {len(self.queued_items)} items:<br><br>"
|
f"Processed {len(self.queued_items)} items:<br><br>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
for idx, item in enumerate(self.queued_items, 1):
|
for idx, item in enumerate(self.queued_items, 1):
|
||||||
output = getattr(item, "output_path", None)
|
output = getattr(item, "output_path", None)
|
||||||
if not output:
|
if not output:
|
||||||
@@ -1834,7 +1933,7 @@ class abogen(QWidget):
|
|||||||
else self.selected_file
|
else self.selected_file
|
||||||
)
|
)
|
||||||
# Only repopulate if not cleared by queue
|
# 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):
|
if display_path and os.path.exists(display_path):
|
||||||
self.input_box.set_file_info(display_path)
|
self.input_box.set_file_info(display_path)
|
||||||
else:
|
else:
|
||||||
@@ -1873,7 +1972,10 @@ class abogen(QWidget):
|
|||||||
self.open_file_btn.setVisible(show_open_file_button)
|
self.open_file_btn.setVisible(show_open_file_button)
|
||||||
|
|
||||||
# Only show finish_widget if queue is done
|
# 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
|
# Queue finished, show finish screen
|
||||||
self.controls_widget.hide()
|
self.controls_widget.hide()
|
||||||
self.finish_widget.show()
|
self.finish_widget.show()
|
||||||
@@ -1938,7 +2040,7 @@ class abogen(QWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Only repopulate if not cleared by queue
|
# 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):
|
if display_path and os.path.exists(display_path):
|
||||||
self.input_box.set_file_info(display_path)
|
self.input_box.set_file_info(display_path)
|
||||||
else:
|
else:
|
||||||
@@ -2031,6 +2133,7 @@ class abogen(QWidget):
|
|||||||
voice_to_cache = voice_formula
|
voice_to_cache = voice_formula
|
||||||
if self.selected_profile_name:
|
if self.selected_profile_name:
|
||||||
from abogen.voice_profiles import load_profiles
|
from abogen.voice_profiles import load_profiles
|
||||||
|
|
||||||
entry = load_profiles().get(self.selected_profile_name, {})
|
entry = load_profiles().get(self.selected_profile_name, {})
|
||||||
lang_to_cache = entry.get("language")
|
lang_to_cache = entry.get("language")
|
||||||
else:
|
else:
|
||||||
@@ -2053,7 +2156,9 @@ class abogen(QWidget):
|
|||||||
cache_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME, "preview_cache")
|
cache_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME, "preview_cache")
|
||||||
|
|
||||||
if "*" in voice_to_cache: # Voice formula
|
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
|
else: # Single voice
|
||||||
voice_id = voice_to_cache
|
voice_id = voice_to_cache
|
||||||
|
|
||||||
@@ -2096,13 +2201,17 @@ class abogen(QWidget):
|
|||||||
try:
|
try:
|
||||||
# Ensure pygame mixer is initialized for the audio thread
|
# Ensure pygame mixer is initialized for the audio thread
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
if not pygame.mixer.get_init():
|
if not pygame.mixer.get_init():
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
|
|
||||||
self.play_audio_thread = PlayAudioThread(cached_path)
|
self.play_audio_thread = PlayAudioThread(cached_path)
|
||||||
self.play_audio_thread.finished.connect(cleanup_cached_play)
|
self.play_audio_thread.finished.connect(cleanup_cached_play)
|
||||||
self.play_audio_thread.error.connect(
|
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()
|
self.play_audio_thread.start()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2192,23 +2301,29 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
def _play_preview_audio(self, from_cache=True): # from_cache default is now False
|
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 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
|
temp_wav = self.preview_thread.temp_wav
|
||||||
elif from_cache: # This case is now handled before calling _play_preview_audio
|
elif from_cache: # This case is now handled before calling _play_preview_audio
|
||||||
cached_path = self._get_preview_cache_path()
|
cached_path = self._get_preview_cache_path()
|
||||||
if cached_path and os.path.exists(cached_path):
|
if cached_path and os.path.exists(cached_path):
|
||||||
temp_wav = cached_path
|
temp_wav = cached_path
|
||||||
else: # Should not happen if cache check was done
|
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()
|
self._preview_cleanup()
|
||||||
return
|
return
|
||||||
else: # Should have temp_wav from preview_thread or handled by cache check
|
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()
|
self._preview_cleanup()
|
||||||
return
|
return
|
||||||
|
|
||||||
if not temp_wav:
|
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(
|
self._show_error_message_box(
|
||||||
"Preview Error", "Preview error: No audio generated."
|
"Preview Error", "Preview error: No audio generated."
|
||||||
)
|
)
|
||||||
@@ -2216,7 +2331,8 @@ class abogen(QWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# stop loading animation, switch to stop icon
|
# 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.preview_playing = True
|
||||||
self.btn_preview.setIcon(self.stop_icon)
|
self.btn_preview.setIcon(self.stop_icon)
|
||||||
self.btn_preview.setToolTip("Stop preview")
|
self.btn_preview.setToolTip("Stop preview")
|
||||||
@@ -2224,9 +2340,16 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
def cleanup():
|
def cleanup():
|
||||||
# Only remove if not from cache AND it's a temp file from VoicePreviewThread
|
# 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:
|
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)
|
os.remove(temp_wav)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -2235,6 +2358,7 @@ class abogen(QWidget):
|
|||||||
try:
|
try:
|
||||||
# Ensure pygame mixer is initialized for the audio thread
|
# Ensure pygame mixer is initialized for the audio thread
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
if not pygame.mixer.get_init():
|
if not pygame.mixer.get_init():
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
|
|
||||||
@@ -2266,9 +2390,11 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
def _preview_cleanup(self):
|
def _preview_cleanup(self):
|
||||||
self.preview_playing = False
|
self.preview_playing = False
|
||||||
if hasattr(self, 'loading_movie'): self.loading_movie.stop()
|
if hasattr(self, "loading_movie"):
|
||||||
|
self.loading_movie.stop()
|
||||||
try:
|
try:
|
||||||
if hasattr(self, 'loading_movie'): self.loading_movie.frameChanged.disconnect()
|
if hasattr(self, "loading_movie"):
|
||||||
|
self.loading_movie.frameChanged.disconnect()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Ignore error if not connected
|
pass # Ignore error if not connected
|
||||||
self.btn_preview.setIcon(self.play_icon)
|
self.btn_preview.setIcon(self.play_icon)
|
||||||
@@ -2301,10 +2427,12 @@ class abogen(QWidget):
|
|||||||
):
|
):
|
||||||
if not hasattr(self, "_conversion_lock"):
|
if not hasattr(self, "_conversion_lock"):
|
||||||
self._conversion_lock = threading.Lock()
|
self._conversion_lock = threading.Lock()
|
||||||
|
|
||||||
def _cancel():
|
def _cancel():
|
||||||
with self._conversion_lock:
|
with self._conversion_lock:
|
||||||
self.conversion_thread.cancel() # <-- Use cancel() method
|
self.conversion_thread.cancel() # <-- Use cancel() method
|
||||||
self.conversion_thread.wait()
|
self.conversion_thread.wait()
|
||||||
|
|
||||||
threading.Thread(target=_cancel, daemon=True).start()
|
threading.Thread(target=_cancel, daemon=True).start()
|
||||||
|
|
||||||
self.is_converting = False
|
self.is_converting = False
|
||||||
@@ -2323,7 +2451,7 @@ class abogen(QWidget):
|
|||||||
else self.selected_file
|
else self.selected_file
|
||||||
)
|
)
|
||||||
# Only repopulate if not cleared by queue
|
# 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):
|
if display_path and os.path.exists(display_path):
|
||||||
self.input_box.set_file_info(display_path)
|
self.input_box.set_file_info(display_path)
|
||||||
else:
|
else:
|
||||||
@@ -2409,9 +2537,10 @@ class abogen(QWidget):
|
|||||||
def is_windows_dark_mode():
|
def is_windows_dark_mode():
|
||||||
try:
|
try:
|
||||||
import winreg
|
import winreg
|
||||||
|
|
||||||
with winreg.OpenKey(
|
with winreg.OpenKey(
|
||||||
winreg.HKEY_CURRENT_USER,
|
winreg.HKEY_CURRENT_USER,
|
||||||
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
|
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
||||||
) as key:
|
) as key:
|
||||||
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
|
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
|
||||||
return value == 0
|
return value == 0
|
||||||
@@ -2436,14 +2565,23 @@ class abogen(QWidget):
|
|||||||
palette.setColor(QPalette.ColorRole.Button, button_bg)
|
palette.setColor(QPalette.ColorRole.Button, button_bg)
|
||||||
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
|
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
|
||||||
# Disabled roles
|
# Disabled roles
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, disabled_fg)
|
palette.setColor(
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_fg)
|
QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, disabled_fg
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, disabled_fg)
|
)
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Base, dark_bg)
|
palette.setColor(
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Button, dark_bg)
|
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)
|
app.setPalette(palette)
|
||||||
|
|
||||||
|
|
||||||
def set_light_palette():
|
def set_light_palette():
|
||||||
palette = QPalette()
|
palette = QPalette()
|
||||||
disabled_fg = QColor(COLORS["LIGHT_DISABLED"])
|
disabled_fg = QColor(COLORS["LIGHT_DISABLED"])
|
||||||
@@ -2457,11 +2595,25 @@ class abogen(QWidget):
|
|||||||
palette.setColor(QPalette.ColorRole.Button, Qt.GlobalColor.white)
|
palette.setColor(QPalette.ColorRole.Button, Qt.GlobalColor.white)
|
||||||
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black)
|
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.black)
|
||||||
# Disabled roles
|
# Disabled roles
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, disabled_fg)
|
palette.setColor(
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, disabled_fg)
|
QPalette.ColorGroup.Disabled, QPalette.ColorRole.WindowText, 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(
|
||||||
palette.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Button, Qt.GlobalColor.white)
|
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)
|
app.setPalette(palette)
|
||||||
|
|
||||||
# --- Dark title bar support for Windows ---
|
# --- Dark title bar support for Windows ---
|
||||||
@@ -2473,12 +2625,19 @@ class abogen(QWidget):
|
|||||||
set_window_attribute = ctypes.windll.dwmapi.DwmSetWindowAttribute
|
set_window_attribute = ctypes.windll.dwmapi.DwmSetWindowAttribute
|
||||||
hwnd = int(window.winId())
|
hwnd = int(window.winId())
|
||||||
value = ctypes.c_int(2 if enable else 0)
|
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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Main logic
|
# 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:
|
if dark_mode:
|
||||||
app.setStyle("Fusion")
|
app.setStyle("Fusion")
|
||||||
set_dark_palette()
|
set_dark_palette()
|
||||||
@@ -2512,7 +2671,10 @@ class abogen(QWidget):
|
|||||||
delattr(app, "_dark_titlebar_event_filter")
|
delattr(app, "_dark_titlebar_event_filter")
|
||||||
|
|
||||||
def get_dark_mode():
|
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(
|
app._dark_titlebar_event_filter = DarkTitleBarEventFilter(
|
||||||
is_windows, get_dark_mode, set_title_bar_dark_mode
|
is_windows, get_dark_mode, set_title_bar_dark_mode
|
||||||
)
|
)
|
||||||
@@ -2554,7 +2716,9 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
# Add separate chapters format option
|
# Add separate chapters format option
|
||||||
separate_chapters_format_menu = QMenu("Separate chapters audio format", self)
|
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 = QActionGroup(self)
|
||||||
format_group.setExclusive(True)
|
format_group.setExclusive(True)
|
||||||
@@ -2563,7 +2727,11 @@ class abogen(QWidget):
|
|||||||
format_action = QAction(format_option, self)
|
format_action = QAction(format_option, self)
|
||||||
format_action.setCheckable(True)
|
format_action.setCheckable(True)
|
||||||
format_action.setChecked(self.separate_chapters_format == format_option)
|
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)
|
format_group.addAction(format_action)
|
||||||
separate_chapters_format_menu.addAction(format_action)
|
separate_chapters_format_menu.addAction(format_action)
|
||||||
|
|
||||||
@@ -2584,7 +2752,11 @@ class abogen(QWidget):
|
|||||||
# Add shortcut to desktop (Windows or Linux)
|
# Add shortcut to desktop (Windows or Linux)
|
||||||
if platform.system() == "Windows" or platform.system() == "Linux":
|
if platform.system() == "Windows" or platform.system() == "Linux":
|
||||||
# Use extended label on 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 = QAction(label, self)
|
||||||
add_shortcut_action.triggered.connect(self.add_shortcut_to_desktop)
|
add_shortcut_action.triggered.connect(self.add_shortcut_to_desktop)
|
||||||
menu.addAction(add_shortcut_action)
|
menu.addAction(add_shortcut_action)
|
||||||
@@ -2673,7 +2845,9 @@ class abogen(QWidget):
|
|||||||
target = os.path.join(python_dir, "Scripts", "abogen.exe")
|
target = os.path.join(python_dir, "Scripts", "abogen.exe")
|
||||||
if not os.path.exists(target):
|
if not os.path.exists(target):
|
||||||
QMessageBox.critical(
|
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
|
return
|
||||||
|
|
||||||
@@ -2683,13 +2857,19 @@ class abogen(QWidget):
|
|||||||
icon = target # Create a more direct PowerShell command
|
icon = target # Create a more direct PowerShell command
|
||||||
shortcut_ps = shortcut_path.replace("'", "''").replace("\\", "\\\\")
|
shortcut_ps = shortcut_path.replace("'", "''").replace("\\", "\\\\")
|
||||||
target_ps = target.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("\\", "\\\\")
|
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()"
|
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
|
# 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()
|
proc.wait()
|
||||||
|
|
||||||
if proc.returncode == 0:
|
if proc.returncode == 0:
|
||||||
@@ -2702,17 +2882,20 @@ class abogen(QWidget):
|
|||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self,
|
self,
|
||||||
"Shortcut Error",
|
"Shortcut Error",
|
||||||
f"PowerShell failed with exit code: {proc.returncode}"
|
f"PowerShell failed with exit code: {proc.returncode}",
|
||||||
)
|
)
|
||||||
elif platform.system() == "Linux":
|
elif platform.system() == "Linux":
|
||||||
desktop = user_desktop_dir()
|
desktop = user_desktop_dir()
|
||||||
if not desktop or not os.path.isdir(desktop):
|
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
|
return
|
||||||
|
|
||||||
shortcut_path = os.path.join(desktop, "abogen.desktop")
|
shortcut_path = os.path.join(desktop, "abogen.desktop")
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
found = shutil.which("abogen")
|
found = shutil.which("abogen")
|
||||||
if found:
|
if found:
|
||||||
target = found
|
target = found
|
||||||
@@ -2728,7 +2911,11 @@ class abogen(QWidget):
|
|||||||
if os.path.exists(target_fallback):
|
if os.path.exists(target_fallback):
|
||||||
target = target_fallback
|
target = target_fallback
|
||||||
else:
|
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
|
return
|
||||||
|
|
||||||
icon_path = get_resource_path("abogen.assets", "icon.png")
|
icon_path = get_resource_path("abogen.assets", "icon.png")
|
||||||
@@ -2763,6 +2950,7 @@ Categories=AudioVideo;Audio;Utility;
|
|||||||
)
|
)
|
||||||
if reply == QMessageBox.Yes:
|
if reply == QMessageBox.Yes:
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
user_app_dir = os.path.expanduser("~/.local/share/applications")
|
user_app_dir = os.path.expanduser("~/.local/share/applications")
|
||||||
os.makedirs(user_app_dir, exist_ok=True)
|
os.makedirs(user_app_dir, exist_ok=True)
|
||||||
user_entry = os.path.join(user_app_dir, "abogen.desktop")
|
user_entry = os.path.join(user_app_dir, "abogen.desktop")
|
||||||
@@ -2782,7 +2970,9 @@ Categories=AudioVideo;Audio;Utility;
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
QMessageBox.information(
|
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:
|
except Exception as e:
|
||||||
@@ -3057,9 +3247,7 @@ Categories=AudioVideo;Audio;Utility;
|
|||||||
result_msg += f"\nAlso deleted {deleted_preview_count} preview cache file{'s' if deleted_preview_count != 1 else ''}."
|
result_msg += f"\nAlso deleted {deleted_preview_count} preview cache file{'s' if deleted_preview_count != 1 else ''}."
|
||||||
|
|
||||||
# Show results
|
# Show results
|
||||||
QMessageBox.information(
|
QMessageBox.information(self, "Temporary Files Cleared", result_msg)
|
||||||
self, "Temporary Files Cleared", result_msg
|
|
||||||
)
|
|
||||||
|
|
||||||
# If currently selected file is in the temp directory, clear the UI
|
# If currently selected file is in the temp directory, clear the UI
|
||||||
if (
|
if (
|
||||||
@@ -3139,4 +3327,3 @@ Categories=AudioVideo;Audio;Utility;
|
|||||||
|
|
||||||
def show_model_download_warning(self, title, message):
|
def show_model_download_warning(self, title, message):
|
||||||
QMessageBox.information(self, title, message)
|
QMessageBox.information(self, title, message)
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -1,16 +1,20 @@
|
|||||||
log_callback = None
|
log_callback = None
|
||||||
show_warning_signal_emitter = None # Renamed for clarity
|
show_warning_signal_emitter = None # Renamed for clarity
|
||||||
|
|
||||||
|
|
||||||
def set_log_callback(cb):
|
def set_log_callback(cb):
|
||||||
global log_callback
|
global log_callback
|
||||||
log_callback = cb
|
log_callback = cb
|
||||||
|
|
||||||
|
|
||||||
def set_show_warning_signal_emitter(emitter): # Renamed for clarity
|
def set_show_warning_signal_emitter(emitter): # Renamed for clarity
|
||||||
global show_warning_signal_emitter
|
global show_warning_signal_emitter
|
||||||
show_warning_signal_emitter = emitter
|
show_warning_signal_emitter = emitter
|
||||||
|
|
||||||
|
|
||||||
from huggingface_hub import hf_hub_download
|
from huggingface_hub import hf_hub_download
|
||||||
|
|
||||||
|
|
||||||
def tracked_hf_hub_download(*args, **kwargs):
|
def tracked_hf_hub_download(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
local_kwargs = dict(kwargs)
|
local_kwargs = dict(kwargs)
|
||||||
@@ -22,7 +26,10 @@ def tracked_hf_hub_download(*args, **kwargs):
|
|||||||
if filename.endswith(".pth"):
|
if filename.endswith(".pth"):
|
||||||
msg = f"\nDownloading model '{filename}' from Hugging Face ({repo_id}). This may take a while. Please wait..."
|
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
|
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:
|
else:
|
||||||
msg = f"\nDownloading '{filename}' from Hugging Face ({repo_id}). Please wait..."
|
msg = f"\nDownloading '{filename}' from Hugging Face ({repo_id}). Please wait..."
|
||||||
if log_callback:
|
if log_callback:
|
||||||
@@ -32,5 +39,7 @@ def tracked_hf_hub_download(*args, **kwargs):
|
|||||||
print(msg, flush=True)
|
print(msg, flush=True)
|
||||||
return hf_hub_download(*args, **kwargs)
|
return hf_hub_download(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
import huggingface_hub
|
import huggingface_hub
|
||||||
|
|
||||||
huggingface_hub.hf_hub_download = tracked_hf_hub_download
|
huggingface_hub.hf_hub_download = tracked_hf_hub_download
|
||||||
+3
-1
@@ -1,18 +1,20 @@
|
|||||||
import gpustat
|
import gpustat
|
||||||
|
|
||||||
|
|
||||||
def check():
|
def check():
|
||||||
try:
|
try:
|
||||||
stats = gpustat.new_query()
|
stats = gpustat.new_query()
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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:
|
for gpu in stats.gpus:
|
||||||
name = gpu.name.lower()
|
name = gpu.name.lower()
|
||||||
if any(keyword in name for keyword in nvidia_keywords):
|
if any(keyword in name for keyword in nvidia_keywords):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
stats = gpustat.new_query()
|
stats = gpustat.new_query()
|
||||||
for gpu in stats.gpus:
|
for gpu in stats.gpus:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ if sys.stderr is None:
|
|||||||
if platform.system() == "Darwin" and platform.processor() == "arm":
|
if platform.system() == "Darwin" and platform.processor() == "arm":
|
||||||
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
|
||||||
|
|
||||||
|
|
||||||
# Custom message handler to filter out specific Qt warnings
|
# Custom message handler to filter out specific Qt warnings
|
||||||
def qt_message_handler(mode, context, message):
|
def qt_message_handler(mode, context, message):
|
||||||
if "Wayland does not support QWindow::requestActivate()" in message:
|
if "Wayland does not support QWindow::requestActivate()" in message:
|
||||||
|
|||||||
+113
-51
@@ -20,6 +20,7 @@ from abogen.constants import COLORS
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from PyQt5.QtGui import QFontMetrics
|
from PyQt5.QtGui import QFontMetrics
|
||||||
|
|
||||||
|
|
||||||
class ElidedLabel(QLabel):
|
class ElidedLabel(QLabel):
|
||||||
def __init__(self, text, parent=None):
|
def __init__(self, text, parent=None):
|
||||||
super().__init__(text, parent)
|
super().__init__(text, parent)
|
||||||
@@ -40,6 +41,7 @@ class ElidedLabel(QLabel):
|
|||||||
def fullText(self):
|
def fullText(self):
|
||||||
return self._full_text
|
return self._full_text
|
||||||
|
|
||||||
|
|
||||||
class QueueListItemWidget(QWidget):
|
class QueueListItemWidget(QWidget):
|
||||||
def __init__(self, file_name, char_count):
|
def __init__(self, file_name, char_count):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -47,6 +49,7 @@ class QueueListItemWidget(QWidget):
|
|||||||
layout.setContentsMargins(12, 0, 6, 0)
|
layout.setContentsMargins(12, 0, 6, 0)
|
||||||
layout.setSpacing(0)
|
layout.setSpacing(0)
|
||||||
import os
|
import os
|
||||||
|
|
||||||
name_label = ElidedLabel(os.path.basename(file_name))
|
name_label = ElidedLabel(os.path.basename(file_name))
|
||||||
char_label = QLabel(f"Chars: {char_count}")
|
char_label = QLabel(f"Chars: {char_count}")
|
||||||
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
|
char_label.setStyleSheet(f"color: {COLORS['LIGHT_DISABLED']};")
|
||||||
@@ -56,6 +59,7 @@ class QueueListItemWidget(QWidget):
|
|||||||
layout.addWidget(char_label, 0)
|
layout.addWidget(char_label, 0)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
|
||||||
class DroppableQueueListWidget(QListWidget):
|
class DroppableQueueListWidget(QListWidget):
|
||||||
def __init__(self, parent_dialog):
|
def __init__(self, parent_dialog):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -73,7 +77,7 @@ class DroppableQueueListWidget(QListWidget):
|
|||||||
def dragEnterEvent(self, event):
|
def dragEnterEvent(self, event):
|
||||||
if event.mimeData().hasUrls():
|
if event.mimeData().hasUrls():
|
||||||
for url in event.mimeData().urls():
|
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.resize(self.size())
|
||||||
self.drag_overlay.setVisible(True)
|
self.drag_overlay.setVisible(True)
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
@@ -84,7 +88,7 @@ class DroppableQueueListWidget(QListWidget):
|
|||||||
def dragMoveEvent(self, event):
|
def dragMoveEvent(self, event):
|
||||||
if event.mimeData().hasUrls():
|
if event.mimeData().hasUrls():
|
||||||
for url in event.mimeData().urls():
|
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()
|
event.acceptProposedAction()
|
||||||
return
|
return
|
||||||
event.ignore()
|
event.ignore()
|
||||||
@@ -96,7 +100,11 @@ class DroppableQueueListWidget(QListWidget):
|
|||||||
def dropEvent(self, event):
|
def dropEvent(self, event):
|
||||||
self.drag_overlay.setVisible(False)
|
self.drag_overlay.setVisible(False)
|
||||||
if event.mimeData().hasUrls():
|
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:
|
if file_paths:
|
||||||
self.parent_dialog.add_files_from_paths(file_paths)
|
self.parent_dialog.add_files_from_paths(file_paths)
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
@@ -107,14 +115,17 @@ class DroppableQueueListWidget(QListWidget):
|
|||||||
|
|
||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
super().resizeEvent(event)
|
super().resizeEvent(event)
|
||||||
if hasattr(self, 'drag_overlay'):
|
if hasattr(self, "drag_overlay"):
|
||||||
self.drag_overlay.resize(self.size())
|
self.drag_overlay.resize(self.size())
|
||||||
|
|
||||||
|
|
||||||
class QueueManager(QDialog):
|
class QueueManager(QDialog):
|
||||||
def __init__(self, parent, queue: list, title="Queue Manager", size=(600, 700)):
|
def __init__(self, parent, queue: list, title="Queue Manager", size=(600, 700)):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.queue = queue
|
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
|
self.parent = parent
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setContentsMargins(15, 15, 15, 15) # set main layout margins
|
layout.setContentsMargins(15, 15, 15, 15) # set main layout margins
|
||||||
@@ -141,10 +152,12 @@ class QueueManager(QDialog):
|
|||||||
# Overlay label for empty queue
|
# Overlay label for empty queue
|
||||||
self.empty_overlay = QLabel(
|
self.empty_overlay = QLabel(
|
||||||
"Drag and drop your text files here or use the 'Add files' button.",
|
"Drag and drop your text files here or use the 'Add files' button.",
|
||||||
self.listwidget
|
self.listwidget,
|
||||||
)
|
)
|
||||||
self.empty_overlay.setAlignment(Qt.AlignCenter)
|
self.empty_overlay.setAlignment(Qt.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.setWordWrap(True)
|
||||||
self.empty_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
|
self.empty_overlay.setAttribute(Qt.WA_TransparentForMouseEvents, True)
|
||||||
self.empty_overlay.hide()
|
self.empty_overlay.hide()
|
||||||
@@ -208,13 +221,14 @@ class QueueManager(QDialog):
|
|||||||
file_name = item.file_name
|
file_name = item.file_name
|
||||||
display_name = file_name
|
display_name = file_name
|
||||||
import os
|
import os
|
||||||
|
|
||||||
if os.path.sep in file_name:
|
if os.path.sep in file_name:
|
||||||
display_name = os.path.basename(file_name)
|
display_name = os.path.basename(file_name)
|
||||||
# Get icon for the file
|
# Get icon for the file
|
||||||
icon = icon_provider.icon(QFileInfo(file_name))
|
icon = icon_provider.icon(QFileInfo(file_name))
|
||||||
list_item = QListWidgetItem()
|
list_item = QListWidgetItem()
|
||||||
# Set tooltip with detailed info
|
# Set tooltip with detailed info
|
||||||
output_folder = getattr(item, 'output_folder', '')
|
output_folder = getattr(item, "output_folder", "")
|
||||||
tooltip = (
|
tooltip = (
|
||||||
f"<b>Path:</b> {file_name}<br>"
|
f"<b>Path:</b> {file_name}<br>"
|
||||||
f"<b>Language:</b> {getattr(item, 'lang_code', '')}<br>"
|
f"<b>Language:</b> {getattr(item, 'lang_code', '')}<br>"
|
||||||
@@ -222,7 +236,7 @@ class QueueManager(QDialog):
|
|||||||
f"<b>Voice:</b> {getattr(item, 'voice', '')}<br>"
|
f"<b>Voice:</b> {getattr(item, 'voice', '')}<br>"
|
||||||
f"<b>Save Option:</b> {getattr(item, 'save_option', '')}<br>"
|
f"<b>Save Option:</b> {getattr(item, 'save_option', '')}<br>"
|
||||||
)
|
)
|
||||||
if output_folder not in (None, '', 'None'):
|
if output_folder not in (None, "", "None"):
|
||||||
tooltip += f"<b>Output Folder:</b> {output_folder}<br>"
|
tooltip += f"<b>Output Folder:</b> {output_folder}<br>"
|
||||||
tooltip += (
|
tooltip += (
|
||||||
f"<b>Subtitle Mode:</b> {getattr(item, 'subtitle_mode', '')}<br>"
|
f"<b>Subtitle Mode:</b> {getattr(item, 'subtitle_mode', '')}<br>"
|
||||||
@@ -233,7 +247,7 @@ class QueueManager(QDialog):
|
|||||||
list_item.setToolTip(tooltip)
|
list_item.setToolTip(tooltip)
|
||||||
list_item.setIcon(icon)
|
list_item.setIcon(icon)
|
||||||
# Use custom widget for display
|
# Use custom widget for display
|
||||||
char_count = getattr(item, 'total_char_count', 0)
|
char_count = getattr(item, "total_char_count", 0)
|
||||||
widget = QueueListItemWidget(file_name, char_count)
|
widget = QueueListItemWidget(file_name, char_count)
|
||||||
self.listwidget.addItem(list_item)
|
self.listwidget.addItem(list_item)
|
||||||
self.listwidget.setItemWidget(list_item, widget)
|
self.listwidget.setItemWidget(list_item, widget)
|
||||||
@@ -244,6 +258,7 @@ class QueueManager(QDialog):
|
|||||||
if not items:
|
if not items:
|
||||||
return
|
return
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
# Remove by index to ensure correct mapping
|
# Remove by index to ensure correct mapping
|
||||||
rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
|
rows = sorted([self.listwidget.row(item) for item in items], reverse=True)
|
||||||
# Warn user if removing multiple files
|
# Warn user if removing multiple files
|
||||||
@@ -253,7 +268,7 @@ class QueueManager(QDialog):
|
|||||||
"Confirm Remove",
|
"Confirm Remove",
|
||||||
f"Are you sure you want to remove {len(rows)} selected items from the queue?",
|
f"Are you sure you want to remove {len(rows)} selected items from the queue?",
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
QMessageBox.No
|
QMessageBox.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.Yes:
|
||||||
return
|
return
|
||||||
@@ -265,19 +280,22 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
def clear_queue(self):
|
def clear_queue(self):
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
if len(self.queue) > 1:
|
if len(self.queue) > 1:
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Confirm Clear Queue",
|
"Confirm Clear Queue",
|
||||||
f"Are you sure you want to clear {len(self.queue)} items from the queue?",
|
f"Are you sure you want to clear {len(self.queue)} items from the queue?",
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
QMessageBox.No
|
QMessageBox.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.Yes:
|
||||||
return
|
return
|
||||||
self.queue.clear()
|
self.queue.clear()
|
||||||
self.listwidget.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.empty_overlay.show() # Show the overlay when queue is empty
|
||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
|
|
||||||
@@ -290,56 +308,74 @@ class QueueManager(QDialog):
|
|||||||
parent = self.parent
|
parent = self.parent
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
# lang_code: use parent's get_voice_formula and get_selected_lang
|
# 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()
|
voice_formula = parent.get_voice_formula()
|
||||||
attrs['lang_code'] = parent.get_selected_lang(voice_formula)
|
attrs["lang_code"] = parent.get_selected_lang(voice_formula)
|
||||||
attrs['voice'] = voice_formula
|
attrs["voice"] = voice_formula
|
||||||
else:
|
else:
|
||||||
attrs['lang_code'] = getattr(parent, 'selected_lang', '')
|
attrs["lang_code"] = getattr(parent, "selected_lang", "")
|
||||||
attrs['voice'] = getattr(parent, 'selected_voice', '')
|
attrs["voice"] = getattr(parent, "selected_voice", "")
|
||||||
# speed
|
# speed
|
||||||
if hasattr(parent, 'speed_slider'):
|
if hasattr(parent, "speed_slider"):
|
||||||
attrs['speed'] = parent.speed_slider.value() / 100.0
|
attrs["speed"] = parent.speed_slider.value() / 100.0
|
||||||
else:
|
else:
|
||||||
attrs['speed'] = getattr(parent, 'speed', 1.0)
|
attrs["speed"] = getattr(parent, "speed", 1.0)
|
||||||
# save_option
|
# save_option
|
||||||
attrs['save_option'] = getattr(parent, 'save_option', '')
|
attrs["save_option"] = getattr(parent, "save_option", "")
|
||||||
# output_folder
|
# output_folder
|
||||||
attrs['output_folder'] = getattr(parent, 'selected_output_folder', '')
|
attrs["output_folder"] = getattr(parent, "selected_output_folder", "")
|
||||||
# subtitle_mode
|
# subtitle_mode
|
||||||
if hasattr(parent, 'get_actual_subtitle_mode'):
|
if hasattr(parent, "get_actual_subtitle_mode"):
|
||||||
attrs['subtitle_mode'] = parent.get_actual_subtitle_mode()
|
attrs["subtitle_mode"] = parent.get_actual_subtitle_mode()
|
||||||
else:
|
else:
|
||||||
attrs['subtitle_mode'] = getattr(parent, 'subtitle_mode', '')
|
attrs["subtitle_mode"] = getattr(parent, "subtitle_mode", "")
|
||||||
# output_format
|
# output_format
|
||||||
attrs['output_format'] = getattr(parent, 'selected_format', '')
|
attrs["output_format"] = getattr(parent, "selected_format", "")
|
||||||
# total_char_count
|
# total_char_count
|
||||||
attrs['total_char_count'] = getattr(parent, 'char_count', '')
|
attrs["total_char_count"] = getattr(parent, "char_count", "")
|
||||||
# replace_single_newlines
|
# replace_single_newlines
|
||||||
attrs['replace_single_newlines'] = getattr(parent, 'replace_single_newlines', False)
|
attrs["replace_single_newlines"] = getattr(
|
||||||
|
parent, "replace_single_newlines", False
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# fallback: empty values
|
# fallback: empty values
|
||||||
attrs = {k: '' for k in [
|
attrs = {
|
||||||
'lang_code', 'speed', 'voice', 'save_option',
|
k: ""
|
||||||
'output_folder', 'subtitle_mode', 'output_format', 'total_char_count', 'replace_single_newlines']}
|
for k in [
|
||||||
|
"lang_code",
|
||||||
|
"speed",
|
||||||
|
"voice",
|
||||||
|
"save_option",
|
||||||
|
"output_folder",
|
||||||
|
"subtitle_mode",
|
||||||
|
"output_format",
|
||||||
|
"total_char_count",
|
||||||
|
"replace_single_newlines",
|
||||||
|
]
|
||||||
|
}
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def add_files_from_paths(self, file_paths):
|
def add_files_from_paths(self, file_paths):
|
||||||
from abogen.utils import calculate_text_length
|
from abogen.utils import calculate_text_length
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
import os
|
import os
|
||||||
|
|
||||||
current_attrs = self.get_current_attributes()
|
current_attrs = self.get_current_attributes()
|
||||||
duplicates = []
|
duplicates = []
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
|
|
||||||
class QueueItem:
|
class QueueItem:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
item = QueueItem()
|
item = QueueItem()
|
||||||
item.file_name = file_path
|
item.file_name = file_path
|
||||||
for attr, value in current_attrs.items():
|
for attr, value in current_attrs.items():
|
||||||
setattr(item, attr, value)
|
setattr(item, attr, value)
|
||||||
# Read file content and calculate total_char_count using calculate_text_length
|
# Read file content and calculate total_char_count using calculate_text_length
|
||||||
try:
|
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()
|
file_content = f.read()
|
||||||
item.total_char_count = calculate_text_length(file_content)
|
item.total_char_count = calculate_text_length(file_content)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -348,16 +384,26 @@ class QueueManager(QDialog):
|
|||||||
is_duplicate = False
|
is_duplicate = False
|
||||||
for queued_item in self.queue:
|
for queued_item in self.queue:
|
||||||
if (
|
if (
|
||||||
getattr(queued_item, 'file_name', None) == getattr(item, 'file_name', None) and
|
getattr(queued_item, "file_name", None)
|
||||||
getattr(queued_item, 'lang_code', None) == getattr(item, 'lang_code', None) and
|
== getattr(item, "file_name", None)
|
||||||
getattr(queued_item, 'speed', None) == getattr(item, 'speed', None) and
|
and getattr(queued_item, "lang_code", None)
|
||||||
getattr(queued_item, 'voice', None) == getattr(item, 'voice', None) and
|
== getattr(item, "lang_code", None)
|
||||||
getattr(queued_item, 'save_option', None) == getattr(item, 'save_option', None) and
|
and getattr(queued_item, "speed", None)
|
||||||
getattr(queued_item, 'output_folder', None) == getattr(item, 'output_folder', None) and
|
== getattr(item, "speed", None)
|
||||||
getattr(queued_item, 'subtitle_mode', None) == getattr(item, 'subtitle_mode', None) and
|
and getattr(queued_item, "voice", None)
|
||||||
getattr(queued_item, 'output_format', None) == getattr(item, 'output_format', None) and
|
== getattr(item, "voice", None)
|
||||||
getattr(queued_item, 'total_char_count', None) == getattr(item, 'total_char_count', None) and
|
and getattr(queued_item, "save_option", None)
|
||||||
getattr(queued_item, 'replace_single_newlines', False) == getattr(item, 'replace_single_newlines', False)
|
== 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
|
is_duplicate = True
|
||||||
break
|
break
|
||||||
@@ -369,7 +415,7 @@ class QueueManager(QDialog):
|
|||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self,
|
self,
|
||||||
"Duplicate Item(s)",
|
"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.process_queue()
|
||||||
self.update_button_states()
|
self.update_button_states()
|
||||||
@@ -377,20 +423,23 @@ class QueueManager(QDialog):
|
|||||||
def add_more_files(self):
|
def add_more_files(self):
|
||||||
from PyQt5.QtWidgets import QFileDialog
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
from abogen.utils import calculate_text_length # import the function
|
from abogen.utils import calculate_text_length # import the function
|
||||||
|
|
||||||
# Only allow .txt files
|
# Only allow .txt files
|
||||||
files, _ = QFileDialog.getOpenFileNames(self, "Select .txt files", "", "Text Files (*.txt)")
|
files, _ = QFileDialog.getOpenFileNames(
|
||||||
|
self, "Select .txt files", "", "Text Files (*.txt)"
|
||||||
|
)
|
||||||
if not files:
|
if not files:
|
||||||
return
|
return
|
||||||
self.add_files_from_paths(files)
|
self.add_files_from_paths(files)
|
||||||
|
|
||||||
def resizeEvent(self, event):
|
def resizeEvent(self, event):
|
||||||
super().resizeEvent(event)
|
super().resizeEvent(event)
|
||||||
if hasattr(self, 'empty_overlay'):
|
if hasattr(self, "empty_overlay"):
|
||||||
self.empty_overlay.resize(self.listwidget.size())
|
self.empty_overlay.resize(self.listwidget.size())
|
||||||
|
|
||||||
def update_button_states(self):
|
def update_button_states(self):
|
||||||
# Enable Remove if at least one item is selected, else disable
|
# 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())
|
selected_count = len(self.listwidget.selectedItems())
|
||||||
self.remove_button.setEnabled(selected_count > 0)
|
self.remove_button.setEnabled(selected_count > 0)
|
||||||
if selected_count > 1:
|
if selected_count > 1:
|
||||||
@@ -398,7 +447,7 @@ class QueueManager(QDialog):
|
|||||||
else:
|
else:
|
||||||
self.remove_button.setText("Remove selected")
|
self.remove_button.setText("Remove selected")
|
||||||
# Disable Clear if queue is empty
|
# Disable Clear if queue is empty
|
||||||
if hasattr(self, 'clear_button'):
|
if hasattr(self, "clear_button"):
|
||||||
self.clear_button.setEnabled(bool(self.queue))
|
self.clear_button.setEnabled(bool(self.queue))
|
||||||
|
|
||||||
def show_context_menu(self, pos):
|
def show_context_menu(self, pos):
|
||||||
@@ -406,6 +455,7 @@ class QueueManager(QDialog):
|
|||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
import os
|
import os
|
||||||
|
|
||||||
global_pos = self.listwidget.viewport().mapToGlobal(pos)
|
global_pos = self.listwidget.viewport().mapToGlobal(pos)
|
||||||
selected_items = self.listwidget.selectedItems()
|
selected_items = self.listwidget.selectedItems()
|
||||||
menu = QMenu(self)
|
menu = QMenu(self)
|
||||||
@@ -417,35 +467,45 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
# Add Open file action
|
# Add Open file action
|
||||||
open_file_action = QAction("Open file", self)
|
open_file_action = QAction("Open file", self)
|
||||||
|
|
||||||
def open_file():
|
def open_file():
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
item = selected_items[0]
|
item = selected_items[0]
|
||||||
display_name = item.text()
|
display_name = item.text()
|
||||||
for q in self.queue:
|
for q in self.queue:
|
||||||
if os.path.basename(q.file_name) == display_name:
|
if os.path.basename(q.file_name) == display_name:
|
||||||
if not os.path.exists(q.file_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
|
return
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(q.file_name))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(q.file_name))
|
||||||
break
|
break
|
||||||
|
|
||||||
open_file_action.triggered.connect(open_file)
|
open_file_action.triggered.connect(open_file)
|
||||||
menu.addAction(open_file_action)
|
menu.addAction(open_file_action)
|
||||||
|
|
||||||
# Add Go to folder action
|
# Add Go to folder action
|
||||||
go_to_folder_action = QAction("Go to folder", self)
|
go_to_folder_action = QAction("Go to folder", self)
|
||||||
|
|
||||||
def go_to_folder():
|
def go_to_folder():
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
item = selected_items[0]
|
item = selected_items[0]
|
||||||
display_name = item.text()
|
display_name = item.text()
|
||||||
for q in self.queue:
|
for q in self.queue:
|
||||||
if os.path.basename(q.file_name) == display_name:
|
if os.path.basename(q.file_name) == display_name:
|
||||||
if not os.path.exists(q.file_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
|
return
|
||||||
folder = os.path.dirname(q.file_name)
|
folder = os.path.dirname(q.file_name)
|
||||||
if os.path.exists(folder):
|
if os.path.exists(folder):
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(folder))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(folder))
|
||||||
break
|
break
|
||||||
|
|
||||||
go_to_folder_action.triggered.connect(go_to_folder)
|
go_to_folder_action.triggered.connect(go_to_folder)
|
||||||
menu.addAction(go_to_folder_action)
|
menu.addAction(go_to_folder_action)
|
||||||
|
|
||||||
@@ -466,6 +526,7 @@ class QueueManager(QDialog):
|
|||||||
def reject(self):
|
def reject(self):
|
||||||
# Cancel: restore original queue
|
# Cancel: restore original queue
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
# Warn if user changed a lot (e.g., more than 1 items difference)
|
# Warn if user changed a lot (e.g., more than 1 items difference)
|
||||||
original_count = len(self._original_queue)
|
original_count = len(self._original_queue)
|
||||||
current_count = len(self.queue)
|
current_count = len(self.queue)
|
||||||
@@ -475,7 +536,7 @@ class QueueManager(QDialog):
|
|||||||
"Confirm Cancel",
|
"Confirm Cancel",
|
||||||
f"Are you sure you want to cancel and discard all changes?",
|
f"Are you sure you want to cancel and discard all changes?",
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
QMessageBox.No
|
QMessageBox.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.Yes:
|
||||||
return
|
return
|
||||||
@@ -485,6 +546,7 @@ class QueueManager(QDialog):
|
|||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
if event.key() == Qt.Key_Delete:
|
if event.key() == Qt.Key_Delete:
|
||||||
self.remove_item()
|
self.remove_item()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# represents a queued item - book, chapters, voice, etc.
|
# represents a queued item - book, chapters, voice, etc.
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class QueuedItem:
|
class QueuedItem:
|
||||||
file_name: str
|
file_name: str
|
||||||
|
|||||||
+19
-5
@@ -105,15 +105,17 @@ def clean_text(text, *args, **kwargs):
|
|||||||
|
|
||||||
default_encoding = sys.getfilesystemencoding()
|
default_encoding = sys.getfilesystemencoding()
|
||||||
|
|
||||||
|
|
||||||
def create_process(cmd, stdin=None, text=True, capture_output=False):
|
def create_process(cmd, stdin=None, text=True, capture_output=False):
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Configure root logger to output to console if not already configured
|
# Configure root logger to output to console if not already configured
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
if not root.handlers:
|
if not root.handlers:
|
||||||
handler = logging.StreamHandler(sys.stdout)
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
formatter = logging.Formatter('%(message)s')
|
formatter = logging.Formatter("%(message)s")
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
root.addHandler(handler)
|
root.addHandler(handler)
|
||||||
root.setLevel(logging.INFO)
|
root.setLevel(logging.INFO)
|
||||||
@@ -156,6 +158,7 @@ def create_process(cmd, stdin=None, text=True, capture_output=False):
|
|||||||
|
|
||||||
# Stream output to console in real-time if not capturing
|
# Stream output to console in real-time if not capturing
|
||||||
if proc.stdout and not capture_output:
|
if proc.stdout and not capture_output:
|
||||||
|
|
||||||
def _stream_output(stream):
|
def _stream_output(stream):
|
||||||
if text:
|
if text:
|
||||||
# For text mode, read character by character for real-time output
|
# For text mode, read character by character for real-time output
|
||||||
@@ -174,7 +177,9 @@ def create_process(cmd, stdin=None, text=True, capture_output=False):
|
|||||||
break
|
break
|
||||||
try:
|
try:
|
||||||
# Try to decode binary data for display
|
# 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()
|
sys.stdout.flush()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -220,6 +225,7 @@ def get_gpu_acceleration(enabled):
|
|||||||
try:
|
try:
|
||||||
import torch
|
import torch
|
||||||
from torch.cuda import is_available
|
from torch.cuda import is_available
|
||||||
|
|
||||||
if not enabled:
|
if not enabled:
|
||||||
return "CUDA GPU available but using CPU.", False
|
return "CUDA GPU available but using CPU.", False
|
||||||
if is_available():
|
if is_available():
|
||||||
@@ -227,7 +233,11 @@ def get_gpu_acceleration(enabled):
|
|||||||
# Gather more diagnostic info if CUDA is not available
|
# Gather more diagnostic info if CUDA is not available
|
||||||
try:
|
try:
|
||||||
cuda_devices = torch.cuda.device_count()
|
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:
|
except Exception as e:
|
||||||
cuda_error = str(e)
|
cuda_error = str(e)
|
||||||
return f"CUDA GPU is not available. Using CPU. ({cuda_error})", False
|
return f"CUDA GPU is not available. Using CPU. ({cuda_error})", False
|
||||||
@@ -239,6 +249,7 @@ def prevent_sleep_start():
|
|||||||
system = platform.system()
|
system = platform.system()
|
||||||
if system == "Windows":
|
if system == "Windows":
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
ctypes.windll.kernel32.SetThreadExecutionState(
|
ctypes.windll.kernel32.SetThreadExecutionState(
|
||||||
0x80000000 | 0x00000001 | 0x00000040
|
0x80000000 | 0x00000001 | 0x00000040
|
||||||
)
|
)
|
||||||
@@ -246,19 +257,22 @@ def prevent_sleep_start():
|
|||||||
_sleep_procs["Darwin"] = create_process(["caffeinate"])
|
_sleep_procs["Darwin"] = create_process(["caffeinate"])
|
||||||
elif system == "Linux":
|
elif system == "Linux":
|
||||||
# use a sleep that never exits so inhibition stays active
|
# use a sleep that never exits so inhibition stays active
|
||||||
_sleep_procs["Linux"] = create_process([
|
_sleep_procs["Linux"] = create_process(
|
||||||
|
[
|
||||||
"systemd-inhibit",
|
"systemd-inhibit",
|
||||||
"--what=handle-lid-switch:sleep",
|
"--what=handle-lid-switch:sleep",
|
||||||
"--mode=block",
|
"--mode=block",
|
||||||
"sleep",
|
"sleep",
|
||||||
"infinity",
|
"infinity",
|
||||||
])
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def prevent_sleep_end():
|
def prevent_sleep_end():
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
if system == "Windows":
|
if system == "Windows":
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
ctypes.windll.kernel32.SetThreadExecutionState(0x80000000) # ES_CONTINUOUS
|
ctypes.windll.kernel32.SetThreadExecutionState(0x80000000) # ES_CONTINUOUS
|
||||||
elif system in ("Darwin", "Linux") and _sleep_procs[system]:
|
elif system in ("Darwin", "Linux") and _sleep_procs[system]:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -230,13 +230,15 @@ class VoiceMixer(QWidget):
|
|||||||
appstyle = QApplication.instance().style().objectName().lower()
|
appstyle = QApplication.instance().style().objectName().lower()
|
||||||
if appstyle != "windowsvista":
|
if appstyle != "windowsvista":
|
||||||
# Set custom groove color for disabled state using COLORS["GREY_BACKGROUND"]
|
# Set custom groove color for disabled state using COLORS["GREY_BACKGROUND"]
|
||||||
self.slider.setStyleSheet(f"""
|
self.slider.setStyleSheet(
|
||||||
|
f"""
|
||||||
QSlider::groove:vertical:disabled {{
|
QSlider::groove:vertical:disabled {{
|
||||||
background: {COLORS.get("GREY_BACKGROUND")};
|
background: {COLORS.get("GREY_BACKGROUND")};
|
||||||
width: 4px;
|
width: 4px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}}
|
}}
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Connect controls
|
# Connect controls
|
||||||
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
|
self.slider.valueChanged.connect(lambda val: self.spin_box.setValue(val / 100))
|
||||||
@@ -330,7 +332,9 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self._original_mixed_voice_state = None
|
self._original_mixed_voice_state = None
|
||||||
if parent is not None:
|
if parent is not None:
|
||||||
self._original_profile_name = getattr(parent, "selected_profile_name", 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()
|
profiles = load_profiles()
|
||||||
self._virtual_new_profile = False
|
self._virtual_new_profile = False
|
||||||
if not profiles:
|
if not profiles:
|
||||||
@@ -369,7 +373,9 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
self.profile_list = QListWidget()
|
self.profile_list = QListWidget()
|
||||||
self.profile_list.setSelectionMode(QListWidget.SingleSelection)
|
self.profile_list.setSelectionMode(QListWidget.SingleSelection)
|
||||||
self.profile_list.setSelectionBehavior(QListWidget.SelectRows)
|
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"))
|
icon = QIcon(get_resource_path("abogen.assets", "profile.png"))
|
||||||
if self._virtual_new_profile:
|
if self._virtual_new_profile:
|
||||||
item = QListWidgetItem(icon, "New profile")
|
item = QListWidgetItem(icon, "New profile")
|
||||||
@@ -976,6 +982,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
def _parse_rgba_to_qcolor(self, rgba_str):
|
def _parse_rgba_to_qcolor(self, rgba_str):
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtGui import QColor
|
from PyQt5.QtGui import QColor
|
||||||
|
|
||||||
"""Helper to convert 'rgba(R,G,B,A_float)' string to QColor."""
|
"""Helper to convert 'rgba(R,G,B,A_float)' string to QColor."""
|
||||||
match = re.match(r"rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)", rgba_str)
|
match = re.match(r"rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)", rgba_str)
|
||||||
if match:
|
if match:
|
||||||
@@ -1372,6 +1379,7 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
|
|
||||||
def update_profile_list_colors(self):
|
def update_profile_list_colors(self):
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
profiles = load_profiles()
|
profiles = load_profiles()
|
||||||
for i in range(self.profile_list.count()):
|
for i in range(self.profile_list.count()):
|
||||||
item = self.profile_list.item(i)
|
item = self.profile_list.item(i)
|
||||||
@@ -1383,7 +1391,9 @@ class VoiceFormulaDialog(QDialog):
|
|||||||
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
|
color = self._parse_rgba_to_qcolor(COLORS.get("YELLOW_BACKGROUND"))
|
||||||
item.setData(Qt.BackgroundRole, color)
|
item.setData(Qt.BackgroundRole, color)
|
||||||
else:
|
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", [])
|
weights = profiles.get(name, {}).get("voices", [])
|
||||||
total = 0
|
total = 0
|
||||||
if isinstance(weights, list):
|
if isinstance(weights, list):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
from abogen.constants import VOICES_INTERNAL
|
from abogen.constants import VOICES_INTERNAL
|
||||||
|
|
||||||
|
|
||||||
# Calls parsing and loads the voice to gpu or cpu
|
# Calls parsing and loads the voice to gpu or cpu
|
||||||
def get_new_voice(pipeline, formula, use_gpu):
|
def get_new_voice(pipeline, formula, use_gpu):
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user