mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 21:50:28 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7631cc852 | ||
|
|
6f09efd018 | ||
|
|
f1903eb6ac | ||
|
|
977b2a736c | ||
|
|
1f51e3edd3 | ||
|
|
5f5dc85ae0 | ||
|
|
0b15633995 | ||
|
|
8bc51dd270 | ||
|
|
d16c7603ce | ||
|
|
970fc6c9c0 | ||
|
|
d2f1c12de9 | ||
|
|
5d9409d391 | ||
|
|
0195788a54 | ||
|
|
1fd8fe21b7 | ||
|
|
89a7116c87 | ||
|
|
6b85a08ccb | ||
|
|
b9c3fd7c6c | ||
|
|
a35bea9053 | ||
|
|
e2802a2737 | ||
|
|
e86ad435e5 | ||
|
|
f2b42ef00e |
@@ -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
|
# 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.
|
||||||
|
|||||||
@@ -149,10 +149,12 @@ Here’s 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. |
|
| **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`. |
|
| **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. |
|
| **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 config directory** | Opens the directory where the configuration file is stored. |
|
||||||
| **Open temp directory** | Opens the temporary directory where converted text files are stored. |
|
| **Open cache directory** | Opens the cache directory where converted text files are stored. |
|
||||||
| **Clear temporary files** | Deletes temporary files created during the conversion or preview. |
|
| **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. |
|
| **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'>
|
||||||
@@ -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)
|
> 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`
|
## `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>>
|
<<CHAPTER_MARKER:Chapter Title>>
|
||||||
@@ -268,7 +270,7 @@ Abogen launches automatically inside the container.
|
|||||||
|
|
||||||
Known issues:
|
Known issues:
|
||||||
- Audio preview is not working inside container (ALSA error).
|
- 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/).)
|
(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
@@ -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.5
|
||||||
+18
-13
@@ -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:
|
||||||
self.pdf_doc.close()
|
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()
|
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 = {
|
||||||
@@ -24,6 +31,28 @@ LANGUAGE_DESCRIPTIONS = {
|
|||||||
"z": "Mandarin Chinese",
|
"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
|
# Supported languages for subtitle generation
|
||||||
# Currently, only 'a (American English)' and 'b (British English)' are supported 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.
|
# 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 = {
|
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)",
|
||||||
|
|||||||
+522
-569
File diff suppressed because it is too large
Load Diff
+197
-109
@@ -62,6 +62,7 @@ from abogen.utils import (
|
|||||||
prevent_sleep_end,
|
prevent_sleep_end,
|
||||||
calculate_text_length,
|
calculate_text_length,
|
||||||
get_resource_path,
|
get_resource_path,
|
||||||
|
get_user_cache_path,
|
||||||
LoadPipelineThread,
|
LoadPipelineThread,
|
||||||
)
|
)
|
||||||
from abogen.conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
|
from abogen.conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
|
||||||
@@ -75,6 +76,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 +126,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;"
|
||||||
@@ -551,12 +565,12 @@ class TextboxDialog(QDialog):
|
|||||||
self.reject()
|
self.reject()
|
||||||
else:
|
else:
|
||||||
# Check if we need to warn about overwriting a non-temporary file
|
# 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 = QMessageBox(self)
|
||||||
msg_box.setIcon(QMessageBox.Warning)
|
msg_box.setIcon(QMessageBox.Warning)
|
||||||
msg_box.setWindowTitle("File Overwrite Warning")
|
msg_box.setWindowTitle("File Overwrite Warning")
|
||||||
msg_box.setText(
|
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.setInformativeText("Do you want to continue?")
|
||||||
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
|
||||||
@@ -578,11 +592,11 @@ class TextboxDialog(QDialog):
|
|||||||
|
|
||||||
# Get default filename from original file if editing
|
# Get default filename from original file if editing
|
||||||
initial_path = ""
|
initial_path = ""
|
||||||
if hasattr(self, "non_temp_file_path") and self.non_temp_file_path:
|
if hasattr(self, "non_cache_file_path") and self.non_cache_file_path:
|
||||||
initial_path = self.non_temp_file_path
|
initial_path = self.non_cache_file_path
|
||||||
|
|
||||||
# For EPUB and PDF files, use the displayed_file_path from the main window
|
# 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()
|
main_window = self.parent()
|
||||||
if (
|
if (
|
||||||
hasattr(main_window, "displayed_file_path")
|
hasattr(main_window, "displayed_file_path")
|
||||||
@@ -646,7 +660,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 +714,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 +764,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 +800,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 +960,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)
|
||||||
@@ -1187,7 +1196,7 @@ class abogen(QWidget):
|
|||||||
if book_path.lower().endswith(".pdf"):
|
if book_path.lower().endswith(".pdf"):
|
||||||
self.pdf_has_bookmarks = getattr(dialog, "has_pdf_bookmarks", False)
|
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
|
# Extract base name without extension
|
||||||
base_name = os.path.splitext(os.path.basename(book_path))[0]
|
base_name = os.path.splitext(os.path.basename(book_path))[0]
|
||||||
|
|
||||||
@@ -1197,15 +1206,15 @@ class abogen(QWidget):
|
|||||||
self, "Select Project Folder", "", QFileDialog.ShowDirsOnly
|
self, "Select Project Folder", "", QFileDialog.ShowDirsOnly
|
||||||
)
|
)
|
||||||
if not project_dir:
|
if not project_dir:
|
||||||
# User cancelled, fallback to temp
|
# User cancelled, fallback to cache
|
||||||
self.save_as_project = False
|
self.save_as_project = False
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
|
cache_dir = get_user_cache_path()
|
||||||
else:
|
else:
|
||||||
# Create project folder structure
|
# Create project folder structure
|
||||||
project_name = f"{base_name}_project"
|
project_name = f"{base_name}_project"
|
||||||
project_dir = os.path.join(project_dir, project_name)
|
project_dir = os.path.join(project_dir, project_name)
|
||||||
temp_dir = os.path.join(project_dir, "text")
|
cache_dir = os.path.join(project_dir, "text")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
|
||||||
# Save metadata if available
|
# Save metadata if available
|
||||||
meta_dir = os.path.join(project_dir, "metadata")
|
meta_dir = os.path.join(project_dir, "metadata")
|
||||||
@@ -1260,11 +1269,10 @@ class abogen(QWidget):
|
|||||||
with open(cover_path, "wb") as f:
|
with open(cover_path, "wb") as f:
|
||||||
f.write(dialog.book_metadata["cover_image"])
|
f.write(dialog.book_metadata["cover_image"])
|
||||||
else:
|
else:
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
|
cache_dir = get_user_cache_path()
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
fd, tmp = tempfile.mkstemp(
|
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)
|
os.close(fd)
|
||||||
with open(tmp, "w", encoding="utf-8") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
@@ -1285,13 +1293,13 @@ class abogen(QWidget):
|
|||||||
return
|
return
|
||||||
|
|
||||||
editing = False
|
editing = False
|
||||||
is_temp_file = False
|
is_cache_file = False
|
||||||
# If path is explicitly provided, use it
|
# If path is explicitly provided, use it
|
||||||
if file_path and os.path.exists(file_path):
|
if file_path and os.path.exists(file_path):
|
||||||
editing = True
|
editing = True
|
||||||
edit_file = file_path
|
edit_file = file_path
|
||||||
# Check if this is a temporary file
|
# Check if this is a cache file
|
||||||
is_temp_file = tempfile.gettempdir() in file_path
|
is_cache_file = get_user_cache_path() in file_path
|
||||||
# Otherwise use selected_file if it's a txt file
|
# Otherwise use selected_file if it's a txt file
|
||||||
elif (
|
elif (
|
||||||
self.selected_file_type == "txt"
|
self.selected_file_type == "txt"
|
||||||
@@ -1300,8 +1308,8 @@ class abogen(QWidget):
|
|||||||
):
|
):
|
||||||
editing = True
|
editing = True
|
||||||
edit_file = self.selected_file
|
edit_file = self.selected_file
|
||||||
# Check if this is a temporary file
|
# Check if this is a cache file
|
||||||
is_temp_file = tempfile.gettempdir() in self.selected_file
|
is_cache_file = get_user_cache_path() in self.selected_file
|
||||||
|
|
||||||
dialog = TextboxDialog(self)
|
dialog = TextboxDialog(self)
|
||||||
if editing:
|
if editing:
|
||||||
@@ -1313,10 +1321,10 @@ class abogen(QWidget):
|
|||||||
dialog.text_edit.toPlainText()
|
dialog.text_edit.toPlainText()
|
||||||
) # Store original text
|
) # Store original text
|
||||||
|
|
||||||
# If editing a non-temporary file, alert the user
|
# If editing a non-cache file, alert the user
|
||||||
if not is_temp_file:
|
if not is_cache_file:
|
||||||
dialog.is_non_temp_file = True
|
dialog.is_non_cache_file = True
|
||||||
dialog.non_temp_file_path = edit_file
|
dialog.non_cache_file_path = edit_file
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if dialog.exec_() == QDialog.Accepted:
|
if dialog.exec_() == QDialog.Accepted:
|
||||||
@@ -1334,10 +1342,9 @@ class abogen(QWidget):
|
|||||||
# Hide chapters button since we're using custom text now
|
# Hide chapters button since we're using custom text now
|
||||||
self.input_box.chapters_btn.hide()
|
self.input_box.chapters_btn.hide()
|
||||||
else:
|
else:
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
|
cache_dir = get_user_cache_path()
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
fd, tmp = tempfile.mkstemp(
|
fd, tmp = tempfile.mkstemp(
|
||||||
prefix="abogen_", suffix=".txt", dir=temp_dir
|
prefix="abogen_", suffix=".txt", dir=cache_dir
|
||||||
)
|
)
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
with open(tmp, "w", encoding="utf-8") as f:
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
@@ -1458,16 +1465,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 +1482,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 +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['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 +1989,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 +2032,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 = (
|
||||||
@@ -2153,7 +2152,7 @@ class abogen(QWidget):
|
|||||||
if not lang_to_cache or not voice_to_cache: # Not enough info
|
if not lang_to_cache or not voice_to_cache: # Not enough info
|
||||||
return None
|
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
|
if "*" in voice_to_cache: # Voice formula
|
||||||
voice_id = (
|
voice_id = (
|
||||||
@@ -2444,7 +2443,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 +2482,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 +2503,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):
|
||||||
@@ -2766,19 +2770,30 @@ class abogen(QWidget):
|
|||||||
reveal_config_action.triggered.connect(self.reveal_config_in_explorer)
|
reveal_config_action.triggered.connect(self.reveal_config_in_explorer)
|
||||||
menu.addAction(reveal_config_action)
|
menu.addAction(reveal_config_action)
|
||||||
|
|
||||||
# Add open temp directory option
|
# Add open cache directory option
|
||||||
open_temp_action = QAction("Open temp directory", self)
|
open_cache_action = QAction("Open cache directory", self)
|
||||||
open_temp_action.triggered.connect(self.open_temp_directory)
|
open_cache_action.triggered.connect(self.open_cache_directory)
|
||||||
menu.addAction(open_temp_action)
|
menu.addAction(open_cache_action)
|
||||||
|
|
||||||
# Add clear temporary files option
|
# Add clear cache files option
|
||||||
clear_temp_action = QAction("Clear temporary files", self)
|
clear_cache_action = QAction("Clear cache files", self)
|
||||||
clear_temp_action.triggered.connect(self.clear_temp_files)
|
clear_cache_action.triggered.connect(self.clear_cache_files)
|
||||||
menu.addAction(clear_temp_action)
|
menu.addAction(clear_cache_action)
|
||||||
|
|
||||||
# Add seperator"
|
# Add separator
|
||||||
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 +2801,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 +2818,74 @@ 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
|
||||||
@@ -2811,21 +2899,21 @@ class abogen(QWidget):
|
|||||||
self, "Config Error", f"Could not open config location:\n{e}"
|
self, "Config Error", f"Could not open config location:\n{e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def open_temp_directory(self):
|
def open_cache_directory(self):
|
||||||
"""Open the temporary directory used by the program."""
|
"""Open the cache directory used by the program."""
|
||||||
try:
|
try:
|
||||||
# Get the temp directory path
|
# Get the cache directory path
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
|
cache_dir = get_user_cache_path()
|
||||||
|
|
||||||
# Create the directory if it doesn't exist
|
# Create the directory if it doesn't exist
|
||||||
if not os.path.exists(temp_dir):
|
if not os.path.exists(cache_dir):
|
||||||
os.makedirs(temp_dir)
|
os.makedirs(cache_dir)
|
||||||
|
|
||||||
# Open the directory in file explorer
|
# Open the directory in file explorer
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(temp_dir))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(cache_dir))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(
|
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):
|
def add_shortcut_to_desktop(self):
|
||||||
@@ -3165,23 +3253,23 @@ Categories=AudioVideo;Audio;Utility;
|
|||||||
)
|
)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def clear_temp_files(self):
|
def clear_cache_files(self):
|
||||||
"""Clear temporary files created by the program."""
|
"""Clear cache files created by the program."""
|
||||||
import glob
|
import glob
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get the abogen temp directory
|
# Get the abogen cache directory
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), PROGRAM_NAME)
|
cache_dir = get_user_cache_path()
|
||||||
|
|
||||||
# Find all .txt files in the abogen temp directory
|
# Find all .txt files in the abogen cache directory
|
||||||
pattern = os.path.join(temp_dir, "*.txt")
|
pattern = os.path.join(cache_dir, "*.txt")
|
||||||
temp_files = glob.glob(pattern)
|
cache_files = glob.glob(pattern)
|
||||||
|
|
||||||
# Count the files
|
# Count the files
|
||||||
file_count = len(temp_files)
|
file_count = len(cache_files)
|
||||||
|
|
||||||
# Check for preview 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 = []
|
preview_files = []
|
||||||
if os.path.exists(preview_cache_dir):
|
if os.path.exists(preview_cache_dir):
|
||||||
preview_pattern = os.path.join(preview_cache_dir, "*.wav")
|
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:
|
if file_count == 0 and preview_count == 0:
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self, "No Temporary Files", "No temporary files were found."
|
self, "No Cache Files", "No cache files were found."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a custom message box with checkbox
|
# Create a custom message box with checkbox
|
||||||
msg_box = QMessageBox(self)
|
msg_box = QMessageBox(self)
|
||||||
msg_box.setIcon(QMessageBox.Question)
|
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:
|
if preview_count > 0:
|
||||||
msg_text += f"\nAlso found {preview_count} preview cache file{'s' if preview_count != 1 else ''}."
|
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
|
# Delete the text files
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
for file_path in temp_files:
|
for file_path in cache_files:
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
deleted_count += 1
|
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 ''}."
|
result_msg += f"\nAlso deleted {deleted_preview_count} preview cache file{'s' if deleted_preview_count != 1 else ''}."
|
||||||
|
|
||||||
# Show results
|
# 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 (
|
if (
|
||||||
self.selected_file
|
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")
|
and self.selected_file.endswith(".txt")
|
||||||
):
|
):
|
||||||
self.input_box.clear_input()
|
self.input_box.clear_input()
|
||||||
|
|||||||
+26
-2
@@ -1,20 +1,45 @@
|
|||||||
|
from json import load
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
|
import atexit
|
||||||
|
import signal
|
||||||
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)
|
||||||
|
|
||||||
|
# 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
|
# 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")
|
||||||
@@ -25,7 +50,6 @@ 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:
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
+20
-7
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
@@ -76,14 +74,23 @@ def get_user_config_path():
|
|||||||
if os.path.exists(custom_dir):
|
if os.path.exists(custom_dir):
|
||||||
config_dir = custom_dir
|
config_dir = custom_dir
|
||||||
else:
|
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:
|
else:
|
||||||
# Windows and fallback case
|
# 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")
|
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
|
_sleep_procs = {"Darwin": None, "Linux": None} # Store sleep prevention processes
|
||||||
|
|
||||||
@@ -246,6 +253,8 @@ 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 +265,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