mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b15633995 | ||
|
|
8bc51dd270 | ||
|
|
d16c7603ce | ||
|
|
970fc6c9c0 | ||
|
|
d2f1c12de9 | ||
|
|
5d9409d391 | ||
|
|
0195788a54 | ||
|
|
1fd8fe21b7 | ||
|
|
89a7116c87 | ||
|
|
6b85a08ccb | ||
|
|
b9c3fd7c6c | ||
|
|
a35bea9053 | ||
|
|
e2802a2737 | ||
|
|
e86ad435e5 | ||
|
|
f2b42ef00e |
@@ -1,3 +1,21 @@
|
|||||||
|
# 1.1.3
|
||||||
|
- `M4B (with chapters)` generation is faster now, as it directly generates `m4b` files instead of converting from `wav`, which significantly reduces processing time.
|
||||||
|
- Better sleep state handling for Linux.
|
||||||
|
- The app window now tries to fit the screen if its height would exceed the available display area.
|
||||||
|
- Fixed issue where the app would not restart properly on Windows.
|
||||||
|
- Fixed last sentence/subtitle entry timing in generated subtitles, the end time of the final subtitle entry now correctly matches the end of the audio chunk, preventing zero or invalid timings at the end.
|
||||||
|
|
||||||
|
# v1.1.2
|
||||||
|
- Now you can play the audio files while they are processing.
|
||||||
|
- Audio and subtitle files are now written directly to disk during generation, which significantly reduces memory usage.
|
||||||
|
- Added a better logic for detecting chapters from the epub, mentioned by @jefro108 in #33
|
||||||
|
- Added a new option: `Reset to default settings`, allowing users to reset all settings to their default values.
|
||||||
|
- Added a new option: `Disable Kokoro's internet access`. This lets you prevent Kokoro from downloading models or voices from HuggingFace Hub, which can help avoid long waiting times if your computer is offline.
|
||||||
|
- HuggingFace Hub telemetry is now disabled by default for improved privacy. (HuggingFace Hub is used by Kokoro to download its models)
|
||||||
|
- Potential fix for #37 and #38, where the program was becoming slow while processing large files.
|
||||||
|
- Fixed `Open folder` and `Open file` buttons in the queue manager GUI.
|
||||||
|
- Improvements in code structure.
|
||||||
|
|
||||||
# v1.1.1
|
# v1.1.1
|
||||||
- Fixed adding wrong file in queue for EPUB and PDF files, ensuring the correct file is added to the queue.
|
- 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.
|
- Reformatted the code using Black.
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex
|
|||||||
| **Open temp directory** | Opens the temporary directory where converted text files are stored. |
|
| **Open temp directory** | Opens the temporary directory where converted text files are stored. |
|
||||||
| **Clear temporary files** | Deletes temporary files created during the conversion or preview. |
|
| **Clear temporary files** | Deletes temporary files created during the conversion or preview. |
|
||||||
| **Check for updates at startup** | Automatically checks for updates when the program starts. |
|
| **Check for updates at startup** | Automatically checks for updates when the program starts. |
|
||||||
|
| **Disable Kokoro's internet access** | Prevents Kokoro from downloading models or voices from HuggingFace Hub, useful for offline use. |
|
||||||
|
| **Reset to default settings** | Resets all settings to their default values. |
|
||||||
|
|
||||||
## `Voice Mixer`
|
## `Voice Mixer`
|
||||||
<img title="Abogen Voice Mixer" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/voice_mixer.png'>
|
<img title="Abogen Voice Mixer" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/voice_mixer.png'>
|
||||||
|
|||||||
+2
-2
@@ -230,7 +230,7 @@ if errorlevel 1 (
|
|||||||
|
|
||||||
:: Install setup requirements
|
:: Install setup requirements
|
||||||
echo Installing setup requirements...
|
echo Installing setup requirements...
|
||||||
%PYTHON_CONSOLE_PATH% -m pip install --upgrade setuptools setuptools-scm wheel sphinx hatchling --no-warn-script-location
|
%PYTHON_CONSOLE_PATH% -m pip install --upgrade setuptools setuptools-scm wheel sphinx hatchling editables --no-warn-script-location
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo Failed to install setup requirements.
|
echo Failed to install setup requirements.
|
||||||
pause
|
pause
|
||||||
@@ -250,7 +250,7 @@ if errorlevel 1 (
|
|||||||
echo Checking and installing project dependencies...
|
echo Checking and installing project dependencies...
|
||||||
if exist %PYPROJECT_FILE% (
|
if exist %PYPROJECT_FILE% (
|
||||||
echo Installing project from pyproject.toml...
|
echo Installing project from pyproject.toml...
|
||||||
%PYTHON_CONSOLE_PATH% -m pip install . --no-warn-script-location
|
%PYTHON_CONSOLE_PATH% -m pip install -e . --no-warn-script-location
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo Failed to install from pyproject.toml.
|
echo Failed to install from pyproject.toml.
|
||||||
pause
|
pause
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.1.1
|
1.1.3
|
||||||
+17
-12
@@ -22,7 +22,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QLabel,
|
QLabel,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from utils import clean_text, calculate_text_length
|
from abogen.utils import clean_text, calculate_text_length
|
||||||
import os
|
import os
|
||||||
import logging # Add logging
|
import logging # Add logging
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@@ -240,20 +240,17 @@ class HandlerDialog(QDialog):
|
|||||||
tag.decompose()
|
tag.decompose()
|
||||||
text = clean_text(soup.get_text()).strip()
|
text = clean_text(soup.get_text()).strip()
|
||||||
if text:
|
if text:
|
||||||
# Use doc_href as the identifier
|
|
||||||
self.content_texts[doc_href] = text
|
self.content_texts[doc_href] = text
|
||||||
self.content_lengths[doc_href] = len(text)
|
self.content_lengths[doc_href] = len(text)
|
||||||
# Create a synthetic TOC entry
|
|
||||||
title = f"Chapter {i+1}: {doc_href}"
|
|
||||||
# Try to get a better title from <h1> or <title>
|
|
||||||
h1 = soup.find("h1")
|
|
||||||
if h1 and h1.get_text(strip=True):
|
|
||||||
title = h1.get_text(strip=True)
|
|
||||||
else:
|
|
||||||
title_tag = soup.find("title")
|
|
||||||
if title_tag and title_tag.get_text(strip=True):
|
|
||||||
title = title_tag.get_text(strip=True)
|
|
||||||
|
|
||||||
|
title = None
|
||||||
|
if soup.title and soup.title.string:
|
||||||
|
title = soup.title.string.strip()
|
||||||
|
elif (h1 := soup.find("h1")) and h1.get_text(strip=True):
|
||||||
|
title = h1.get_text(strip=True)
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
title = f"Untitled Chapter {i+1}"
|
||||||
synthetic_toc.append(
|
synthetic_toc.append(
|
||||||
(epub.Link(doc_href, title, doc_href), [])
|
(epub.Link(doc_href, title, doc_href), [])
|
||||||
) # Wrap in tuple and empty list for compatibility
|
) # Wrap in tuple and empty list for compatibility
|
||||||
@@ -1911,5 +1908,13 @@ class HandlerDialog(QDialog):
|
|||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
if self.pdf_doc is not None:
|
if self.pdf_doc is not None:
|
||||||
|
try:
|
||||||
|
if hasattr(self.pdf_doc, "is_closed"):
|
||||||
|
if not self.pdf_doc.is_closed:
|
||||||
self.pdf_doc.close()
|
self.pdf_doc.close()
|
||||||
|
else:
|
||||||
|
# Fallback: try/except close
|
||||||
|
self.pdf_doc.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ VERSION = get_version()
|
|||||||
|
|
||||||
# Settings
|
# Settings
|
||||||
CHAPTER_OPTIONS_COUNTDOWN = 30 # Countdown seconds for chapter options
|
CHAPTER_OPTIONS_COUNTDOWN = 30 # Countdown seconds for chapter options
|
||||||
|
SUBTITLE_FORMATS = [
|
||||||
|
("srt", "SRT (standard)"),
|
||||||
|
("ass_wide", "ASS (wide)"),
|
||||||
|
("ass_narrow", "ASS (narrow)"),
|
||||||
|
("ass_centered_wide", "ASS (centered wide)"),
|
||||||
|
("ass_centered_narrow", "ASS (centered narrow)"),
|
||||||
|
]
|
||||||
|
|
||||||
# Language description mapping
|
# Language description mapping
|
||||||
LANGUAGE_DESCRIPTIONS = {
|
LANGUAGE_DESCRIPTIONS = {
|
||||||
@@ -109,6 +116,7 @@ SAMPLE_VOICE_TEXTS = {
|
|||||||
COLORS = {
|
COLORS = {
|
||||||
"BLUE": "#007dff",
|
"BLUE": "#007dff",
|
||||||
"RED": "#c0392b",
|
"RED": "#c0392b",
|
||||||
|
"ORANGE": "#FFA500",
|
||||||
"GREEN": "#42ad4a",
|
"GREEN": "#42ad4a",
|
||||||
"GREEN_BG": "rgba(66, 173, 73, 0.1)",
|
"GREEN_BG": "rgba(66, 173, 73, 0.1)",
|
||||||
"GREEN_BG_HOVER": "rgba(66, 173, 73, 0.15)",
|
"GREEN_BG_HOVER": "rgba(66, 173, 73, 0.15)",
|
||||||
|
|||||||
+457
-516
File diff suppressed because it is too large
Load Diff
+138
-50
@@ -75,6 +75,7 @@ from abogen.constants import (
|
|||||||
VOICES_INTERNAL,
|
VOICES_INTERNAL,
|
||||||
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
COLORS,
|
COLORS,
|
||||||
|
SUBTITLE_FORMATS,
|
||||||
)
|
)
|
||||||
import threading
|
import threading
|
||||||
from abogen.voice_formula_gui import VoiceFormulaDialog
|
from abogen.voice_formula_gui import VoiceFormulaDialog
|
||||||
@@ -124,6 +125,18 @@ class IconProvider(QFileIconProvider):
|
|||||||
return super().icon(fileInfo)
|
return super().icon(fileInfo)
|
||||||
|
|
||||||
|
|
||||||
|
LOG_COLOR_MAP = {
|
||||||
|
True: COLORS["GREEN"],
|
||||||
|
False: COLORS["RED"],
|
||||||
|
"red": COLORS["RED"],
|
||||||
|
"green": COLORS["GREEN"],
|
||||||
|
"orange": COLORS["ORANGE"],
|
||||||
|
"blue": COLORS["BLUE"],
|
||||||
|
"grey": COLORS["LIGHT_DISABLED"],
|
||||||
|
None: COLORS["LIGHT_DISABLED"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class InputBox(QLabel):
|
class InputBox(QLabel):
|
||||||
# Define CSS styles as class constants
|
# Define CSS styles as class constants
|
||||||
STYLE_DEFAULT = f"border:2px dashed #aaa; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG']}; min-height:100px;"
|
STYLE_DEFAULT = f"border:2px dashed #aaa; border-radius:5px; padding:20px; background:{COLORS['BLUE_BG']}; min-height:100px;"
|
||||||
@@ -646,7 +659,7 @@ class abogen(QWidget):
|
|||||||
None # Add new variable to track the displayed file path
|
None # Add new variable to track the displayed file path
|
||||||
)
|
)
|
||||||
# Max log lines
|
# Max log lines
|
||||||
self.log_window_max_lines = self.config.get("log_window_max_lines", 5000)
|
self.log_window_max_lines = self.config.get("log_window_max_lines", 2000)
|
||||||
self.selected_chapters = set()
|
self.selected_chapters = set()
|
||||||
self.last_opened_book_path = None # Track the last opened book path
|
self.last_opened_book_path = None # Track the last opened book path
|
||||||
self.last_output_path = None
|
self.last_output_path = None
|
||||||
@@ -700,9 +713,6 @@ class abogen(QWidget):
|
|||||||
self.queued_items = []
|
self.queued_items = []
|
||||||
self.current_queue_index = 0
|
self.current_queue_index = 0
|
||||||
|
|
||||||
# Log lines
|
|
||||||
self._log_lines = []
|
|
||||||
|
|
||||||
self.initUI()
|
self.initUI()
|
||||||
self.speed_slider.setValue(int(self.config.get("speed", 1.00) * 100))
|
self.speed_slider.setValue(int(self.config.get("speed", 1.00) * 100))
|
||||||
self.update_speed_label()
|
self.update_speed_label()
|
||||||
@@ -753,7 +763,11 @@ class abogen(QWidget):
|
|||||||
self.setWindowTitle(f"{PROGRAM_NAME} v{VERSION}")
|
self.setWindowTitle(f"{PROGRAM_NAME} v{VERSION}")
|
||||||
screen = QApplication.primaryScreen().geometry()
|
screen = QApplication.primaryScreen().geometry()
|
||||||
width, height = 500, 800
|
width, height = 500, 800
|
||||||
x, y = (screen.width() - width) // 2, (screen.height() - height) // 2
|
x = (screen.width() - width) // 2
|
||||||
|
# If desired height is larger than screen, fit to screen height
|
||||||
|
if height > screen.height() - 65:
|
||||||
|
height = screen.height() - 100 # Leave a margin for window borders
|
||||||
|
y = max((screen.height() - height) // 2, 0)
|
||||||
self.setGeometry(x, y, width, height)
|
self.setGeometry(x, y, width, height)
|
||||||
outer_layout = QVBoxLayout()
|
outer_layout = QVBoxLayout()
|
||||||
outer_layout.setContentsMargins(15, 15, 15, 15)
|
outer_layout.setContentsMargins(15, 15, 15, 15)
|
||||||
@@ -785,6 +799,7 @@ class abogen(QWidget):
|
|||||||
container_layout.addWidget(self.queue_row_widget)
|
container_layout.addWidget(self.queue_row_widget)
|
||||||
self.log_text = QTextEdit(self)
|
self.log_text = QTextEdit(self)
|
||||||
self.log_text.setReadOnly(True)
|
self.log_text.setReadOnly(True)
|
||||||
|
self.log_text.setUndoRedoEnabled(False)
|
||||||
self.log_text.setFrameStyle(QTextEdit.NoFrame)
|
self.log_text.setFrameStyle(QTextEdit.NoFrame)
|
||||||
self.log_text.setStyleSheet("QTextEdit { border: none; }")
|
self.log_text.setStyleSheet("QTextEdit { border: none; }")
|
||||||
self.log_text.hide()
|
self.log_text.hide()
|
||||||
@@ -944,14 +959,7 @@ class abogen(QWidget):
|
|||||||
self.subtitle_format_combo.setSizePolicy(
|
self.subtitle_format_combo.setSizePolicy(
|
||||||
QSizePolicy.Expanding, QSizePolicy.Fixed
|
QSizePolicy.Expanding, QSizePolicy.Fixed
|
||||||
)
|
)
|
||||||
subtitle_formats = [
|
for value, text in SUBTITLE_FORMATS:
|
||||||
("srt", "SRT (standard)"),
|
|
||||||
("ass_wide", "ASS (wide)"),
|
|
||||||
("ass_narrow", "ASS (narrow)"),
|
|
||||||
("ass_centered_wide", "ASS (centered wide)"),
|
|
||||||
("ass_centered_narrow", "ASS (centered narrow)"),
|
|
||||||
]
|
|
||||||
for value, text in subtitle_formats:
|
|
||||||
self.subtitle_format_combo.addItem(text, value)
|
self.subtitle_format_combo.addItem(text, value)
|
||||||
subtitle_format = self.config.get("subtitle_format", "ass_centered_narrow")
|
subtitle_format = self.config.get("subtitle_format", "ass_centered_narrow")
|
||||||
idx = self.subtitle_format_combo.findData(subtitle_format)
|
idx = self.subtitle_format_combo.findData(subtitle_format)
|
||||||
@@ -1458,16 +1466,12 @@ class abogen(QWidget):
|
|||||||
self.input_box.hide()
|
self.input_box.hide()
|
||||||
self.log_text.show()
|
self.log_text.show()
|
||||||
self.log_text.clear()
|
self.log_text.clear()
|
||||||
self._log_lines = []
|
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
def restore_input_box(self):
|
def restore_input_box(self):
|
||||||
self.log_text.hide()
|
self.log_text.hide()
|
||||||
self.input_box.show()
|
self.input_box.show()
|
||||||
|
|
||||||
def color_text(self, message, color):
|
|
||||||
return f'<span style="color:{color};">{message.replace(chr(10), "<br>")}</span><br>'
|
|
||||||
|
|
||||||
def update_log(self, message):
|
def update_log(self, message):
|
||||||
# Use signal-based approach for thread-safe logging
|
# Use signal-based approach for thread-safe logging
|
||||||
if QThread.currentThread() != QApplication.instance().thread():
|
if QThread.currentThread() != QApplication.instance().thread():
|
||||||
@@ -1479,38 +1483,36 @@ 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"):
|
txt = self.log_text
|
||||||
self._log_lines = []
|
sb = txt.verticalScrollBar()
|
||||||
|
at_bottom = sb.value() == sb.maximum()
|
||||||
|
|
||||||
# Always produce a single HTML line (with color if tuple)
|
cursor = txt.textCursor()
|
||||||
|
cursor.movePosition(QTextCursor.End)
|
||||||
|
|
||||||
|
fmt = cursor.charFormat()
|
||||||
if isinstance(message, tuple):
|
if isinstance(message, tuple):
|
||||||
text, spec = message
|
text, spec = message
|
||||||
color = "green" if spec is True else ("red" if spec is False else spec)
|
fmt.setForeground(QColor(LOG_COLOR_MAP.get(spec, COLORS["LIGHT_DISABLED"])))
|
||||||
html_line = self.color_text(text, color)
|
|
||||||
else:
|
else:
|
||||||
html_line = f"{str(message).replace(chr(10), '<br>')}<br>"
|
text = str(message)
|
||||||
|
fmt.clearForeground()
|
||||||
|
cursor.setCharFormat(fmt)
|
||||||
|
cursor.insertText(text + "\n")
|
||||||
|
|
||||||
self._log_lines.append(html_line)
|
doc = txt.document()
|
||||||
|
excess = doc.blockCount() - self.log_window_max_lines
|
||||||
|
if excess > 0:
|
||||||
|
start = doc.findBlockByNumber(0).position()
|
||||||
|
end = doc.findBlockByNumber(excess).position()
|
||||||
|
trim_cursor = QTextCursor(doc)
|
||||||
|
trim_cursor.setPosition(start)
|
||||||
|
trim_cursor.setPosition(end, QTextCursor.KeepAnchor)
|
||||||
|
trim_cursor.removeSelectedText()
|
||||||
|
|
||||||
# Check if user is at bottom BEFORE inserting
|
|
||||||
sb = self.log_text.verticalScrollBar()
|
|
||||||
at_bottom = sb.value() >= sb.maximum() - 2
|
|
||||||
|
|
||||||
# Trim buffer if needed
|
|
||||||
if len(self._log_lines) > self.log_window_max_lines:
|
|
||||||
self._log_lines = self._log_lines[-self.log_window_max_lines :]
|
|
||||||
self.log_text.clear()
|
|
||||||
for line in self._log_lines:
|
|
||||||
self.log_text.insertHtml(line)
|
|
||||||
else:
|
|
||||||
self.log_text.insertHtml(html_line)
|
|
||||||
|
|
||||||
# If user was at bottom before, scroll to new bottom
|
|
||||||
if at_bottom:
|
if at_bottom:
|
||||||
sb.setValue(sb.maximum())
|
sb.setValue(sb.maximum())
|
||||||
|
|
||||||
QApplication.processEvents()
|
|
||||||
|
|
||||||
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 (
|
if (
|
||||||
@@ -1889,7 +1891,7 @@ class abogen(QWidget):
|
|||||||
f"<span style='color:{COLORS['GREEN']}; font-weight:bold;'>{idx}) {os.path.basename(item.file_name)}</span><br>"
|
f"<span style='color:{COLORS['GREEN']}; font-weight:bold;'>{idx}) {os.path.basename(item.file_name)}</span><br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Language:</span> {item.lang_code}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Language:</span> {item.lang_code}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Voice:</span> {item.voice}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Voice:</span> {item.voice}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Speed:</span> {item.speed * 100:.1f}%<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Speed:</span> {item.speed}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Characters:</span> {item.total_char_count}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Characters:</span> {item.total_char_count}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Input:</span> {item.file_name}<br>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Input:</span> {item.file_name}<br>"
|
||||||
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Output:</span> {output}</span>"
|
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Output:</span> {output}</span>"
|
||||||
@@ -1988,7 +1990,6 @@ class abogen(QWidget):
|
|||||||
else:
|
else:
|
||||||
# More items in queue: clear log and reload for next item
|
# More items in queue: clear log and reload for next item
|
||||||
self.log_text.clear()
|
self.log_text.clear()
|
||||||
self._log_lines = []
|
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
# Start new queued item, if we're using a queued conversion
|
# Start new queued item, if we're using a queued conversion
|
||||||
@@ -2032,7 +2033,6 @@ class abogen(QWidget):
|
|||||||
self.progress_bar.hide()
|
self.progress_bar.hide()
|
||||||
self.restore_input_box()
|
self.restore_input_box()
|
||||||
self.log_text.clear()
|
self.log_text.clear()
|
||||||
self._log_lines = []
|
|
||||||
|
|
||||||
# Use displayed_file_path instead of selected_file for EPUBs or PDFs
|
# Use displayed_file_path instead of selected_file for EPUBs or PDFs
|
||||||
display_path = (
|
display_path = (
|
||||||
@@ -2444,7 +2444,6 @@ class abogen(QWidget):
|
|||||||
self.finish_widget.hide()
|
self.finish_widget.hide()
|
||||||
self.restore_input_box()
|
self.restore_input_box()
|
||||||
self.log_text.clear()
|
self.log_text.clear()
|
||||||
self._log_lines = []
|
|
||||||
display_path = (
|
display_path = (
|
||||||
self.displayed_file_path
|
self.displayed_file_path
|
||||||
if self.displayed_file_path
|
if self.displayed_file_path
|
||||||
@@ -2484,6 +2483,16 @@ class abogen(QWidget):
|
|||||||
self.config["use_gpu"] = self.use_gpu
|
self.config["use_gpu"] = self.use_gpu
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
|
|
||||||
|
def cleanup_conversion_thread(self):
|
||||||
|
# Stop conversion thread
|
||||||
|
if (
|
||||||
|
hasattr(self, "conversion_thread")
|
||||||
|
and self.conversion_thread is not None
|
||||||
|
and self.conversion_thread.isRunning()
|
||||||
|
):
|
||||||
|
self.conversion_thread.cancel()
|
||||||
|
self.conversion_thread.wait()
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
if self.is_converting:
|
if self.is_converting:
|
||||||
box = QMessageBox(self)
|
box = QMessageBox(self)
|
||||||
@@ -2495,16 +2504,12 @@ class abogen(QWidget):
|
|||||||
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
||||||
box.setDefaultButton(QMessageBox.No)
|
box.setDefaultButton(QMessageBox.No)
|
||||||
if box.exec_() == QMessageBox.Yes:
|
if box.exec_() == QMessageBox.Yes:
|
||||||
if (
|
self.cleanup_conversion_thread()
|
||||||
hasattr(self, "conversion_thread")
|
|
||||||
and self.conversion_thread.isRunning()
|
|
||||||
):
|
|
||||||
self.conversion_thread.cancel()
|
|
||||||
self.conversion_thread.wait()
|
|
||||||
event.accept()
|
event.accept()
|
||||||
else:
|
else:
|
||||||
event.ignore()
|
event.ignore()
|
||||||
else:
|
else:
|
||||||
|
self.cleanup_conversion_thread()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
def show_chapter_options_dialog(self, chapter_count):
|
def show_chapter_options_dialog(self, chapter_count):
|
||||||
@@ -2779,6 +2784,17 @@ class abogen(QWidget):
|
|||||||
# Add seperator"
|
# Add seperator"
|
||||||
menu.addSeparator()
|
menu.addSeparator()
|
||||||
|
|
||||||
|
# Add "Disable Kokoro's internet access" option
|
||||||
|
disable_kokoro_action = QAction("Disable Kokoro's internet access", self)
|
||||||
|
disable_kokoro_action.setCheckable(True)
|
||||||
|
disable_kokoro_action.setChecked(
|
||||||
|
self.config.get("disable_kokoro_internet", False)
|
||||||
|
)
|
||||||
|
disable_kokoro_action.triggered.connect(
|
||||||
|
lambda checked: self.toggle_kokoro_internet_access(checked)
|
||||||
|
)
|
||||||
|
menu.addAction(disable_kokoro_action)
|
||||||
|
|
||||||
# Add check for updates option
|
# Add check for updates option
|
||||||
check_updates_action = QAction("Check for updates at startup", self)
|
check_updates_action = QAction("Check for updates at startup", self)
|
||||||
check_updates_action.setCheckable(True)
|
check_updates_action.setCheckable(True)
|
||||||
@@ -2786,6 +2802,11 @@ class abogen(QWidget):
|
|||||||
check_updates_action.triggered.connect(self.toggle_check_updates)
|
check_updates_action.triggered.connect(self.toggle_check_updates)
|
||||||
menu.addAction(check_updates_action)
|
menu.addAction(check_updates_action)
|
||||||
|
|
||||||
|
# Add "Reset to default settings" option
|
||||||
|
reset_defaults_action = QAction("Reset to default settings", self)
|
||||||
|
reset_defaults_action.triggered.connect(self.reset_to_default_settings)
|
||||||
|
menu.addAction(reset_defaults_action)
|
||||||
|
|
||||||
# Add about action
|
# Add about action
|
||||||
about_action = QAction("About", self)
|
about_action = QAction("About", self)
|
||||||
about_action.triggered.connect(self.show_about_dialog)
|
about_action.triggered.connect(self.show_about_dialog)
|
||||||
@@ -2798,6 +2819,73 @@ class abogen(QWidget):
|
|||||||
self.config["replace_single_newlines"] = enabled
|
self.config["replace_single_newlines"] = enabled
|
||||||
save_config(self.config)
|
save_config(self.config)
|
||||||
|
|
||||||
|
def restart_app(self):
|
||||||
|
from PyQt5.QtCore import QProcess
|
||||||
|
import sys
|
||||||
|
exe = sys.executable
|
||||||
|
args = sys.argv
|
||||||
|
|
||||||
|
# On Windows, use .exe if available
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
script_path = args[0]
|
||||||
|
if not script_path.lower().endswith('.exe'):
|
||||||
|
exe_path = os.path.splitext(script_path)[0] + '.exe'
|
||||||
|
if os.path.exists(exe_path):
|
||||||
|
args[0] = exe_path
|
||||||
|
|
||||||
|
QProcess.startDetached(exe, args)
|
||||||
|
QApplication.quit()
|
||||||
|
|
||||||
|
def toggle_kokoro_internet_access(self, disabled):
|
||||||
|
if disabled:
|
||||||
|
message = (
|
||||||
|
"Disabling Kokoro's internet access will block downloads of models and voices from Hugging Face Hub. "
|
||||||
|
"This can make processing faster when there is no internet connection, since no requests will be made. "
|
||||||
|
"The app needs to restart to apply this change.\n\nDo you want to continue?"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = (
|
||||||
|
"Enabling Kokoro's internet access will allow it to download models and voices from Hugging Face Hub. "
|
||||||
|
"The app needs to restart to apply this change.\n\nDo you want to continue?"
|
||||||
|
)
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Restart Required",
|
||||||
|
message,
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
self.config["disable_kokoro_internet"] = disabled
|
||||||
|
save_config(self.config)
|
||||||
|
try:
|
||||||
|
self.restart_app()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Restart Failed", f"Failed to restart the application:\n{e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def reset_to_default_settings(self):
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Reset Settings",
|
||||||
|
"This will reset all settings to their default values and restart the application.\n\nDo you want to continue?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No,
|
||||||
|
)
|
||||||
|
if reply == QMessageBox.Yes:
|
||||||
|
from abogen.utils import get_user_config_path
|
||||||
|
|
||||||
|
config_path = get_user_config_path()
|
||||||
|
try:
|
||||||
|
if os.path.exists(config_path):
|
||||||
|
os.remove(config_path)
|
||||||
|
self.restart_app()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Reset Error", f"Could not reset settings:\n{e}"
|
||||||
|
)
|
||||||
|
|
||||||
def reveal_config_in_explorer(self):
|
def reveal_config_in_explorer(self):
|
||||||
"""Open the configuration file location in file explorer."""
|
"""Open the configuration file location in file explorer."""
|
||||||
from abogen.utils import get_user_config_path
|
from abogen.utils import get_user_config_path
|
||||||
|
|||||||
+17
-1
@@ -1,20 +1,36 @@
|
|||||||
|
from json import load
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
|
import atexit
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
|
from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
|
||||||
|
|
||||||
# Add the directory to Python path
|
# Add the directory to Python path
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
||||||
|
|
||||||
|
from abogen.utils import get_resource_path, load_config, prevent_sleep_end
|
||||||
|
|
||||||
|
# Set Hugging Face Hub environment variables
|
||||||
|
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1" # Disable Hugging Face telemetry
|
||||||
|
os.environ["HF_HUB_ETAG_TIMEOUT"] = "10" # Metadata request timeout (seconds)
|
||||||
|
os.environ["HF_HUB_DOWNLOAD_TIMEOUT"] = "10" # File download timeout (seconds)
|
||||||
|
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1" # Disable symlinks warning
|
||||||
|
if load_config().get("disable_kokoro_internet", False):
|
||||||
|
print("INFO: Kokoro's internet access is disabled.")
|
||||||
|
os.environ["HF_HUB_OFFLINE"] = "1" # Disable Hugging Face Hub internet access
|
||||||
|
|
||||||
from abogen.gui import abogen
|
from abogen.gui import abogen
|
||||||
from abogen.utils import get_resource_path
|
|
||||||
from abogen.constants import PROGRAM_NAME, VERSION
|
from abogen.constants import PROGRAM_NAME, VERSION
|
||||||
|
|
||||||
# Set environment variables for AMD ROCm
|
# Set environment variables for AMD ROCm
|
||||||
os.environ["MIOPEN_FIND_MODE"] = "FAST"
|
os.environ["MIOPEN_FIND_MODE"] = "FAST"
|
||||||
os.environ["MIOPEN_CONV_PRECISE_ROCM_TUNING"] = "0"
|
os.environ["MIOPEN_CONV_PRECISE_ROCM_TUNING"] = "0"
|
||||||
|
|
||||||
|
# Reset sleep states
|
||||||
|
atexit.register(prevent_sleep_end)
|
||||||
|
|
||||||
# Ensure sys.stdout and sys.stderr are valid in GUI mode
|
# Ensure sys.stdout and sys.stderr are valid in GUI mode
|
||||||
if sys.stdout is None:
|
if sys.stdout is None:
|
||||||
sys.stdout = open(os.devnull, "w")
|
sys.stdout = open(os.devnull, "w")
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ class QueueManager(QDialog):
|
|||||||
)
|
)
|
||||||
list_item.setToolTip(tooltip)
|
list_item.setToolTip(tooltip)
|
||||||
list_item.setIcon(icon)
|
list_item.setIcon(icon)
|
||||||
|
list_item.setData(Qt.UserRole, file_name)
|
||||||
# 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)
|
||||||
@@ -472,9 +473,9 @@ class QueueManager(QDialog):
|
|||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
item = selected_items[0]
|
item = selected_items[0]
|
||||||
display_name = item.text()
|
file_path = item.data(Qt.UserRole)
|
||||||
for q in self.queue:
|
for q in self.queue:
|
||||||
if os.path.basename(q.file_name) == display_name:
|
if q.file_name == file_path:
|
||||||
if not os.path.exists(q.file_name):
|
if not os.path.exists(q.file_name):
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, "File Not Found", f"The file does not exist."
|
self, "File Not Found", f"The file does not exist."
|
||||||
@@ -493,9 +494,9 @@ class QueueManager(QDialog):
|
|||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
item = selected_items[0]
|
item = selected_items[0]
|
||||||
display_name = item.text()
|
file_path = item.data(Qt.UserRole)
|
||||||
for q in self.queue:
|
for q in self.queue:
|
||||||
if os.path.basename(q.file_name) == display_name:
|
if q.file_name == file_path:
|
||||||
if not os.path.exists(q.file_name):
|
if not os.path.exists(q.file_name):
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, "File Not Found", f"The file does not exist."
|
self, "File Not Found", f"The file does not exist."
|
||||||
|
|||||||
+7
-4
@@ -7,8 +7,6 @@ import subprocess
|
|||||||
import re
|
import re
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
# suppress warnings and disable HF hub symlink warnings
|
|
||||||
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
|
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
|
||||||
@@ -246,6 +244,7 @@ def get_gpu_acceleration(enabled):
|
|||||||
|
|
||||||
|
|
||||||
def prevent_sleep_start():
|
def prevent_sleep_start():
|
||||||
|
from abogen.constants import PROGRAM_NAME
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
if system == "Windows":
|
if system == "Windows":
|
||||||
import ctypes
|
import ctypes
|
||||||
@@ -256,11 +255,15 @@ def prevent_sleep_start():
|
|||||||
elif system == "Darwin":
|
elif system == "Darwin":
|
||||||
_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
|
# Add program name and reason for inhibition
|
||||||
|
program_name = PROGRAM_NAME
|
||||||
|
reason = "Prevent sleep during abogen process"
|
||||||
_sleep_procs["Linux"] = create_process(
|
_sleep_procs["Linux"] = create_process(
|
||||||
[
|
[
|
||||||
"systemd-inhibit",
|
"systemd-inhibit",
|
||||||
"--what=handle-lid-switch:sleep",
|
f"--who={program_name}",
|
||||||
|
f"--why={reason}",
|
||||||
|
"--what=sleep",
|
||||||
"--mode=block",
|
"--mode=block",
|
||||||
"sleep",
|
"sleep",
|
||||||
"infinity",
|
"infinity",
|
||||||
|
|||||||
Reference in New Issue
Block a user