21 Commits
Author SHA1 Message Date
Deniz Şafak c7631cc852 v1.1.5 2025-07-16 16:48:36 +03:00
Deniz Şafak 6f09efd018 Improved automatic filename suffixing 2025-07-16 16:47:18 +03:00
Deniz Şafak f1903eb6ac Fixed sleep prevention process not ending if program exited using Ctrl+C or kill 2025-07-16 15:35:45 +03:00
Deniz Şafak 977b2a736c Changed the temporary directory path to user's cache directory 2025-07-16 15:03:10 +03:00
Deniz Şafak 1f51e3edd3 Add extra metadata to m4b even there are no chapters 2025-07-16 08:07:03 +03:00
Deniz Şafak 5f5dc85ae0 v1.1.4 - Fixed extra metadata information not being saved to M4B files 2025-07-16 07:14:10 +03:00
Deniz Şafak 0b15633995 v1.1.3 2025-07-16 06:27:22 +03:00
Deniz Şafak 8bc51dd270 App window now tries to fit the screen 2025-07-16 06:26:59 +03:00
Deniz Şafak d16c7603ce Better logic for M4B generation that makes it very fast 2025-07-16 05:15:08 +03:00
Deniz Şafak 970fc6c9c0 Better sleep state handling for Linux 2025-07-16 01:14:14 +03:00
Deniz Şafak d2f1c12de9 Fixed last sentence/subtitle entry timing in generated subtitles 2025-07-16 01:00:27 +03:00
Deniz Şafak 5d9409d391 Fix restart app in Windows 2025-07-15 12:23:15 +03:00
Deniz Şafak 0195788a54 v1.1.2 2025-07-15 11:59:27 +03:00
Deniz Şafak 1fd8fe21b7 Reformat using black 2025-07-15 11:57:43 +03:00
Deniz Şafak 89a7116c87 Generate subtitles dynamically like sound files, improvements in code 2025-07-15 11:50:22 +03:00
Deniz Şafak 6b85a08ccb Add new options and improvements for hfhub 2025-07-15 10:24:05 +03:00
Deniz Şafak b9c3fd7c6c Added a better logic for detecting chapters from the epub 2025-07-15 09:05:18 +03:00
Deniz Şafak a35bea9053 Directly write to audio file instead of RAM, improvements 2025-07-15 08:50:26 +03:00
Deniz Şafak e2802a2737 Potential fix for #37 and #38 2025-07-15 02:11:31 +03:00
Deniz Şafak e86ad435e5 Fixed Open folder and Open file buttons in the queue manager GUI 2025-07-14 12:09:44 +03:00
Deniz Şafak f2b42ef00e Windows installer build in editable mode 2025-07-12 01:52:17 +03:00
11 changed files with 857 additions and 712 deletions
+29
View File
@@ -1,3 +1,32 @@
# 1.1.5
- Changed the temporary directory path to user's cache directory, which is more appropriate for storing cache files and avoids issues with unintended cleanup.
- Fixed the isssue where extra metadata information was not being saved to M4B files when they have no chapters, ensuring that all metadata is correctly written to the output file.
- Fixed sleep prevention process not ending if program exited using Ctrl+C or kill.
- Improved automatic filename suffixing to better prevent overwriting files with the same name, even if they have different extensions.
- Improvements in code and documentation.
# 1.1.4
- Fixed extra metadata information not being saved to M4B files, ensuring that all metadata is correctly written to the output file.
- Reformatted the code using Black for better readability and consistency.
# 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, fixes the issue mentioned by @Milor123 in #39
- 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
- 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.
+7 -5
View File
@@ -149,10 +149,12 @@ Heres Abogen in action: in this demo, it processes 3,000 characters of tex
| **Configure max lines in log window** | Configures the maximum number of lines to display in the log window. |
| **Separate chapters audio format** | Configures the audio format for separate chapters as `wav`, `flac`, `mp3`, or `opus`. |
| **Create desktop shortcut** | Creates a shortcut on your desktop for easy access. |
| **Open config.json directory** | Opens the directory where the configuration file is 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. |
| **Open config directory** | Opens the directory where the configuration file is stored. |
| **Open cache directory** | Opens the cache directory where converted text files are stored. |
| **Clear cache files** | Deletes cache files created during the conversion or preview. |
| **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`
<img title="Abogen Voice Mixer" src='https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/voice_mixer.png'>
@@ -172,7 +174,7 @@ Abogen will process each item in the queue automatically, saving outputs as conf
> Special thanks to [@jborza](https://github.com/jborza) for adding queue mode in PR [#35](https://github.com/denizsafak/abogen/pull/35)
## `About Chapter Markers`
When you process ePUB or PDF files, Abogen converts them into text files stored in your temporary directory. When you click "Edit," you're actually modifying these converted text files. In these text files, you'll notice tags that look like this:
When you process ePUB or PDF files, Abogen converts them into text files stored in your cache directory. When you click "Edit," you're actually modifying these converted text files. In these text files, you'll notice tags that look like this:
```
<<CHAPTER_MARKER:Chapter Title>>
@@ -268,7 +270,7 @@ Abogen launches automatically inside the container.
Known issues:
- Audio preview is not working inside container (ALSA error).
- `Open temp directory` and `Open configuration directory` options in settings not working. (Tried pcmanfm, did not work with Abogen).
- `Open cache directory` and `Open configuration directory` options in settings not working. (Tried pcmanfm, did not work with Abogen).
(Special thanks to [@geo38](https://www.reddit.com/user/geo38/) from Reddit, who provided the Dockerfile and instructions in [this comment](https://www.reddit.com/r/selfhosted/comments/1k8x1yo/comment/mpe0bz8/).)
+2 -2
View File
@@ -230,7 +230,7 @@ if errorlevel 1 (
:: Install 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 (
echo Failed to install setup requirements.
pause
@@ -250,7 +250,7 @@ if errorlevel 1 (
echo Checking and installing project dependencies...
if exist %PYPROJECT_FILE% (
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 (
echo Failed to install from pyproject.toml.
pause
+1 -1
View File
@@ -1 +1 @@
1.1.1
1.1.5
+17 -12
View File
@@ -22,7 +22,7 @@ from PyQt5.QtWidgets import (
QLabel,
)
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 logging # Add logging
import urllib.parse
@@ -240,20 +240,17 @@ class HandlerDialog(QDialog):
tag.decompose()
text = clean_text(soup.get_text()).strip()
if text:
# Use doc_href as the identifier
self.content_texts[doc_href] = 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(
(epub.Link(doc_href, title, doc_href), [])
) # Wrap in tuple and empty list for compatibility
@@ -1911,5 +1908,13 @@ class HandlerDialog(QDialog):
def closeEvent(self, event):
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()
else:
# Fallback: try/except close
self.pdf_doc.close()
except Exception:
pass
event.accept()
+30
View File
@@ -10,6 +10,13 @@ VERSION = get_version()
# Settings
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_DESCRIPTIONS = {
@@ -24,6 +31,28 @@ LANGUAGE_DESCRIPTIONS = {
"z": "Mandarin Chinese",
}
# Supported sound formats
SUPPORTED_SOUND_FORMATS = [
"wav",
"mp3",
"opus",
"m4b",
"flac",
]
# Supported subtitle formats
SUPPORTED_SUBTITLE_FORMATS = [
"srt",
"ass",
]
# Supported input formats
SUPPORTED_INPUT_FORMATS = [
"epub",
"pdf",
"txt",
]
# Supported languages for subtitle generation
# Currently, only 'a (American English)' and 'b (British English)' are supported for subtitle generation.
# This is because tokens that contain timestamps are not generated for other languages in the Kokoro pipeline.
@@ -109,6 +138,7 @@ SAMPLE_VOICE_TEXTS = {
COLORS = {
"BLUE": "#007dff",
"RED": "#c0392b",
"ORANGE": "#FFA500",
"GREEN": "#42ad4a",
"GREEN_BG": "rgba(66, 173, 73, 0.1)",
"GREEN_BG_HOVER": "rgba(66, 173, 73, 0.15)",
+477 -524
View File
File diff suppressed because it is too large Load Diff
+197 -109
View File
@@ -62,6 +62,7 @@ from abogen.utils import (
prevent_sleep_end,
calculate_text_length,
get_resource_path,
get_user_cache_path,
LoadPipelineThread,
)
from abogen.conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
@@ -75,6 +76,7 @@ from abogen.constants import (
VOICES_INTERNAL,
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
COLORS,
SUBTITLE_FORMATS,
)
import threading
from abogen.voice_formula_gui import VoiceFormulaDialog
@@ -124,6 +126,18 @@ class IconProvider(QFileIconProvider):
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):
# 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;"
@@ -551,12 +565,12 @@ class TextboxDialog(QDialog):
self.reject()
else:
# Check if we need to warn about overwriting a non-temporary file
if hasattr(self, "is_non_temp_file") and self.is_non_temp_file:
if hasattr(self, "is_non_cache_file") and self.is_non_cache_file:
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Warning)
msg_box.setWindowTitle("File Overwrite Warning")
msg_box.setText(
f"You are about to overwrite the original file:\n{self.non_temp_file_path}"
f"You are about to overwrite the original file:\n{self.non_cache_file_path}"
)
msg_box.setInformativeText("Do you want to continue?")
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
@@ -578,11 +592,11 @@ class TextboxDialog(QDialog):
# Get default filename from original file if editing
initial_path = ""
if hasattr(self, "non_temp_file_path") and self.non_temp_file_path:
initial_path = self.non_temp_file_path
if hasattr(self, "non_cache_file_path") and self.non_cache_file_path:
initial_path = self.non_cache_file_path
# For EPUB and PDF files, use the displayed_file_path from the main window
# This gives a better filename instead of the temporary file path
# This gives a better filename instead of the cache file path
main_window = self.parent()
if (
hasattr(main_window, "displayed_file_path")
@@ -646,7 +660,7 @@ class abogen(QWidget):
None # Add new variable to track the displayed file path
)
# 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.last_opened_book_path = None # Track the last opened book path
self.last_output_path = None
@@ -700,9 +714,6 @@ class abogen(QWidget):
self.queued_items = []
self.current_queue_index = 0
# Log lines
self._log_lines = []
self.initUI()
self.speed_slider.setValue(int(self.config.get("speed", 1.00) * 100))
self.update_speed_label()
@@ -753,7 +764,11 @@ class abogen(QWidget):
self.setWindowTitle(f"{PROGRAM_NAME} v{VERSION}")
screen = QApplication.primaryScreen().geometry()
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)
outer_layout = QVBoxLayout()
outer_layout.setContentsMargins(15, 15, 15, 15)
@@ -785,6 +800,7 @@ class abogen(QWidget):
container_layout.addWidget(self.queue_row_widget)
self.log_text = QTextEdit(self)
self.log_text.setReadOnly(True)
self.log_text.setUndoRedoEnabled(False)
self.log_text.setFrameStyle(QTextEdit.NoFrame)
self.log_text.setStyleSheet("QTextEdit { border: none; }")
self.log_text.hide()
@@ -944,14 +960,7 @@ class abogen(QWidget):
self.subtitle_format_combo.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Fixed
)
subtitle_formats = [
("srt", "SRT (standard)"),
("ass_wide", "ASS (wide)"),
("ass_narrow", "ASS (narrow)"),
("ass_centered_wide", "ASS (centered wide)"),
("ass_centered_narrow", "ASS (centered narrow)"),
]
for value, text in subtitle_formats:
for value, text in SUBTITLE_FORMATS:
self.subtitle_format_combo.addItem(text, value)
subtitle_format = self.config.get("subtitle_format", "ass_centered_narrow")
idx = self.subtitle_format_combo.findData(subtitle_format)
@@ -1187,7 +1196,7 @@ class abogen(QWidget):
if book_path.lower().endswith(".pdf"):
self.pdf_has_bookmarks = getattr(dialog, "has_pdf_bookmarks", False)
# Use "abogen" prefix for temporary files
# Use "abogen" prefix for cache files
# Extract base name without extension
base_name = os.path.splitext(os.path.basename(book_path))[0]
@@ -1197,15 +1206,15 @@ class abogen(QWidget):
self, "Select Project Folder", "", QFileDialog.ShowDirsOnly
)
if not project_dir:
# User cancelled, fallback to temp
# User cancelled, fallback to cache
self.save_as_project = False
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
cache_dir = get_user_cache_path()
else:
# Create project folder structure
project_name = f"{base_name}_project"
project_dir = os.path.join(project_dir, project_name)
temp_dir = os.path.join(project_dir, "text")
os.makedirs(temp_dir, exist_ok=True)
cache_dir = os.path.join(project_dir, "text")
os.makedirs(cache_dir, exist_ok=True)
# Save metadata if available
meta_dir = os.path.join(project_dir, "metadata")
@@ -1260,11 +1269,10 @@ class abogen(QWidget):
with open(cover_path, "wb") as f:
f.write(dialog.book_metadata["cover_image"])
else:
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
os.makedirs(temp_dir, exist_ok=True)
cache_dir = get_user_cache_path()
fd, tmp = tempfile.mkstemp(
prefix=f"{base_name}_", suffix=".txt", dir=temp_dir
prefix=f"{base_name}_", suffix=".txt", dir=cache_dir
)
os.close(fd)
with open(tmp, "w", encoding="utf-8") as f:
@@ -1285,13 +1293,13 @@ class abogen(QWidget):
return
editing = False
is_temp_file = False
is_cache_file = False
# If path is explicitly provided, use it
if file_path and os.path.exists(file_path):
editing = True
edit_file = file_path
# Check if this is a temporary file
is_temp_file = tempfile.gettempdir() in file_path
# Check if this is a cache file
is_cache_file = get_user_cache_path() in file_path
# Otherwise use selected_file if it's a txt file
elif (
self.selected_file_type == "txt"
@@ -1300,8 +1308,8 @@ class abogen(QWidget):
):
editing = True
edit_file = self.selected_file
# Check if this is a temporary file
is_temp_file = tempfile.gettempdir() in self.selected_file
# Check if this is a cache file
is_cache_file = get_user_cache_path() in self.selected_file
dialog = TextboxDialog(self)
if editing:
@@ -1313,10 +1321,10 @@ class abogen(QWidget):
dialog.text_edit.toPlainText()
) # Store original text
# If editing a non-temporary file, alert the user
if not is_temp_file:
dialog.is_non_temp_file = True
dialog.non_temp_file_path = edit_file
# If editing a non-cache file, alert the user
if not is_cache_file:
dialog.is_non_cache_file = True
dialog.non_cache_file_path = edit_file
except Exception:
pass
if dialog.exec_() == QDialog.Accepted:
@@ -1334,10 +1342,9 @@ class abogen(QWidget):
# Hide chapters button since we're using custom text now
self.input_box.chapters_btn.hide()
else:
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
os.makedirs(temp_dir, exist_ok=True)
cache_dir = get_user_cache_path()
fd, tmp = tempfile.mkstemp(
prefix="abogen_", suffix=".txt", dir=temp_dir
prefix="abogen_", suffix=".txt", dir=cache_dir
)
os.close(fd)
with open(tmp, "w", encoding="utf-8") as f:
@@ -1458,16 +1465,12 @@ class abogen(QWidget):
self.input_box.hide()
self.log_text.show()
self.log_text.clear()
self._log_lines = []
QApplication.processEvents()
def restore_input_box(self):
self.log_text.hide()
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):
# Use signal-based approach for thread-safe logging
if QThread.currentThread() != QApplication.instance().thread():
@@ -1479,38 +1482,36 @@ class abogen(QWidget):
self._update_log_main_thread(message)
def _update_log_main_thread(self, message):
if not hasattr(self, "_log_lines"):
self._log_lines = []
txt = self.log_text
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):
text, spec = message
color = "green" if spec is True else ("red" if spec is False else spec)
html_line = self.color_text(text, color)
fmt.setForeground(QColor(LOG_COLOR_MAP.get(spec, COLORS["LIGHT_DISABLED"])))
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:
sb.setValue(sb.maximum())
QApplication.processEvents()
def _get_queue_progress_format(self, value=None):
"""Return the progress bar format string for queue mode."""
if (
@@ -1889,7 +1890,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['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']};'>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']};'>Input:</span> {item.file_name}<br>"
f"<span style='color:{COLORS['LIGHT_DISABLED']};'>Output:</span> {output}</span>"
@@ -1988,7 +1989,6 @@ class abogen(QWidget):
else:
# More items in queue: clear log and reload for next item
self.log_text.clear()
self._log_lines = []
QApplication.processEvents()
# Start new queued item, if we're using a queued conversion
@@ -2032,7 +2032,6 @@ class abogen(QWidget):
self.progress_bar.hide()
self.restore_input_box()
self.log_text.clear()
self._log_lines = []
# Use displayed_file_path instead of selected_file for EPUBs or PDFs
display_path = (
@@ -2153,7 +2152,7 @@ class abogen(QWidget):
if not lang_to_cache or not voice_to_cache: # Not enough info
return None
cache_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME, "preview_cache")
cache_dir = get_user_cache_path("preview_cache")
if "*" in voice_to_cache: # Voice formula
voice_id = (
@@ -2444,7 +2443,6 @@ class abogen(QWidget):
self.finish_widget.hide()
self.restore_input_box()
self.log_text.clear()
self._log_lines = []
display_path = (
self.displayed_file_path
if self.displayed_file_path
@@ -2484,6 +2482,16 @@ class abogen(QWidget):
self.config["use_gpu"] = self.use_gpu
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):
if self.is_converting:
box = QMessageBox(self)
@@ -2495,16 +2503,12 @@ class abogen(QWidget):
box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
box.setDefaultButton(QMessageBox.No)
if box.exec_() == QMessageBox.Yes:
if (
hasattr(self, "conversion_thread")
and self.conversion_thread.isRunning()
):
self.conversion_thread.cancel()
self.conversion_thread.wait()
self.cleanup_conversion_thread()
event.accept()
else:
event.ignore()
else:
self.cleanup_conversion_thread()
event.accept()
def show_chapter_options_dialog(self, chapter_count):
@@ -2766,19 +2770,30 @@ class abogen(QWidget):
reveal_config_action.triggered.connect(self.reveal_config_in_explorer)
menu.addAction(reveal_config_action)
# Add open temp directory option
open_temp_action = QAction("Open temp directory", self)
open_temp_action.triggered.connect(self.open_temp_directory)
menu.addAction(open_temp_action)
# Add open cache directory option
open_cache_action = QAction("Open cache directory", self)
open_cache_action.triggered.connect(self.open_cache_directory)
menu.addAction(open_cache_action)
# Add clear temporary files option
clear_temp_action = QAction("Clear temporary files", self)
clear_temp_action.triggered.connect(self.clear_temp_files)
menu.addAction(clear_temp_action)
# Add clear cache files option
clear_cache_action = QAction("Clear cache files", self)
clear_cache_action.triggered.connect(self.clear_cache_files)
menu.addAction(clear_cache_action)
# Add seperator"
# Add separator
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
check_updates_action = QAction("Check for updates at startup", self)
check_updates_action.setCheckable(True)
@@ -2786,6 +2801,11 @@ class abogen(QWidget):
check_updates_action.triggered.connect(self.toggle_check_updates)
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
about_action = QAction("About", self)
about_action.triggered.connect(self.show_about_dialog)
@@ -2798,6 +2818,74 @@ class abogen(QWidget):
self.config["replace_single_newlines"] = enabled
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):
"""Open the configuration file location in file explorer."""
from abogen.utils import get_user_config_path
@@ -2811,21 +2899,21 @@ class abogen(QWidget):
self, "Config Error", f"Could not open config location:\n{e}"
)
def open_temp_directory(self):
"""Open the temporary directory used by the program."""
def open_cache_directory(self):
"""Open the cache directory used by the program."""
try:
# Get the temp directory path
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
# Get the cache directory path
cache_dir = get_user_cache_path()
# Create the directory if it doesn't exist
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
# Open the directory in file explorer
QDesktopServices.openUrl(QUrl.fromLocalFile(temp_dir))
QDesktopServices.openUrl(QUrl.fromLocalFile(cache_dir))
except Exception as e:
QMessageBox.critical(
self, "Temp Directory Error", f"Could not open temp directory:\n{e}"
self, "Cache Directory Error", f"Could not open cache directory:\n{e}"
)
def add_shortcut_to_desktop(self):
@@ -3165,23 +3253,23 @@ Categories=AudioVideo;Audio;Utility;
)
pass
def clear_temp_files(self):
"""Clear temporary files created by the program."""
def clear_cache_files(self):
"""Clear cache files created by the program."""
import glob
try:
# Get the abogen temp directory
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
# Get the abogen cache directory
cache_dir = get_user_cache_path()
# Find all .txt files in the abogen temp directory
pattern = os.path.join(temp_dir, "*.txt")
temp_files = glob.glob(pattern)
# Find all .txt files in the abogen cache directory
pattern = os.path.join(cache_dir, "*.txt")
cache_files = glob.glob(pattern)
# Count the files
file_count = len(temp_files)
file_count = len(cache_files)
# Check for preview cache files
preview_cache_dir = os.path.join(temp_dir, "preview_cache")
preview_cache_dir = os.path.join(cache_dir, "preview_cache")
preview_files = []
if os.path.exists(preview_cache_dir):
preview_pattern = os.path.join(preview_cache_dir, "*.wav")
@@ -3191,16 +3279,16 @@ Categories=AudioVideo;Audio;Utility;
if file_count == 0 and preview_count == 0:
QMessageBox.information(
self, "No Temporary Files", "No temporary files were found."
self, "No Cache Files", "No cache files were found."
)
return
# Create a custom message box with checkbox
msg_box = QMessageBox(self)
msg_box.setIcon(QMessageBox.Question)
msg_box.setWindowTitle("Clear Temporary Files")
msg_box.setWindowTitle("Clear Cache Files")
msg_text = f"Found {file_count} temporary file{'s' if file_count != 1 else ''} in the {PROGRAM_NAME} temp folder."
msg_text = f"Found {file_count} cache file{'s' if file_count != 1 else ''} in the {PROGRAM_NAME} cache folder."
if preview_count > 0:
msg_text += f"\nAlso found {preview_count} preview cache file{'s' if preview_count != 1 else ''}."
@@ -3224,7 +3312,7 @@ Categories=AudioVideo;Audio;Utility;
# Delete the text files
deleted_count = 0
for file_path in temp_files:
for file_path in cache_files:
try:
os.remove(file_path)
deleted_count += 1
@@ -3247,12 +3335,12 @@ Categories=AudioVideo;Audio;Utility;
result_msg += f"\nAlso deleted {deleted_preview_count} preview cache file{'s' if deleted_preview_count != 1 else ''}."
# Show results
QMessageBox.information(self, "Temporary Files Cleared", result_msg)
QMessageBox.information(self, "Cache Files Cleared", result_msg)
# If currently selected file is in the temp directory, clear the UI
# If currently selected file is in the cache directory, clear the UI
if (
self.selected_file
and os.path.dirname(self.selected_file) == temp_dir
and os.path.dirname(self.selected_file) == cache_dir
and self.selected_file.endswith(".txt")
):
self.input_box.clear_input()
+26 -2
View File
@@ -1,20 +1,45 @@
from json import load
import os
import sys
import platform
import atexit
import signal
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import qInstallMessageHandler, QtMsgType
# Add the directory to Python path
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.utils import get_resource_path
from abogen.constants import PROGRAM_NAME, VERSION
# Set environment variables for AMD ROCm
os.environ["MIOPEN_FIND_MODE"] = "FAST"
os.environ["MIOPEN_CONV_PRECISE_ROCM_TUNING"] = "0"
# Reset sleep states
atexit.register(prevent_sleep_end)
# Also handle signals (Ctrl+C, kill, etc.)
def _cleanup_sleep(signum, frame):
prevent_sleep_end()
sys.exit(0)
signal.signal(signal.SIGINT, _cleanup_sleep)
signal.signal(signal.SIGTERM, _cleanup_sleep)
# Ensure sys.stdout and sys.stderr are valid in GUI mode
if sys.stdout is None:
sys.stdout = open(os.devnull, "w")
@@ -25,7 +50,6 @@ if sys.stderr is None:
if platform.system() == "Darwin" and platform.processor() == "arm":
os.environ["PYTORCH_ENABLE_MPS_FALLBACK"] = "1"
# Custom message handler to filter out specific Qt warnings
def qt_message_handler(mode, context, message):
if "Wayland does not support QWindow::requestActivate()" in message:
+5 -4
View File
@@ -246,6 +246,7 @@ class QueueManager(QDialog):
)
list_item.setToolTip(tooltip)
list_item.setIcon(icon)
list_item.setData(Qt.UserRole, file_name)
# Use custom widget for display
char_count = getattr(item, "total_char_count", 0)
widget = QueueListItemWidget(file_name, char_count)
@@ -472,9 +473,9 @@ class QueueManager(QDialog):
from PyQt5.QtWidgets import QMessageBox
item = selected_items[0]
display_name = item.text()
file_path = item.data(Qt.UserRole)
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):
QMessageBox.warning(
self, "File Not Found", f"The file does not exist."
@@ -493,9 +494,9 @@ class QueueManager(QDialog):
from PyQt5.QtWidgets import QMessageBox
item = selected_items[0]
display_name = item.text()
file_path = item.data(Qt.UserRole)
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):
QMessageBox.warning(
self, "File Not Found", f"The file does not exist."
+20 -7
View File
@@ -7,8 +7,6 @@ import subprocess
import re
from threading import Thread
# suppress warnings and disable HF hub symlink warnings
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
warnings.filterwarnings("ignore")
@@ -76,14 +74,23 @@ def get_user_config_path():
if os.path.exists(custom_dir):
config_dir = custom_dir
else:
config_dir = user_config_dir("abogen", appauthor=False, roaming=True)
config_dir = user_config_dir("abogen", appauthor=False, roaming=True, ensure_exists=True)
else:
# Windows and fallback case
config_dir = user_config_dir("abogen", appauthor=False, roaming=True)
config_dir = user_config_dir("abogen", appauthor=False, roaming=True, ensure_exists=True)
os.makedirs(config_dir, exist_ok=True)
return os.path.join(config_dir, "config.json")
# Define cache path
def get_user_cache_path(folder=None):
from platformdirs import user_cache_dir
cache_dir = user_cache_dir("abogen", appauthor=False, opinion=True, ensure_exists=True)
if folder:
cache_dir = os.path.join(cache_dir, folder)
# Ensure the directory exists
os.makedirs(cache_dir, exist_ok=True)
return cache_dir
_sleep_procs = {"Darwin": None, "Linux": None} # Store sleep prevention processes
@@ -246,6 +253,8 @@ def get_gpu_acceleration(enabled):
def prevent_sleep_start():
from abogen.constants import PROGRAM_NAME
system = platform.system()
if system == "Windows":
import ctypes
@@ -256,11 +265,15 @@ def prevent_sleep_start():
elif system == "Darwin":
_sleep_procs["Darwin"] = create_process(["caffeinate"])
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(
[
"systemd-inhibit",
"--what=handle-lid-switch:sleep",
f"--who={program_name}",
f"--why={reason}",
"--what=sleep",
"--mode=block",
"sleep",
"infinity",