From 56b336a52984572895bfb975290b56050b2a38b8 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Fri, 2 May 2025 18:28:17 +0200 Subject: [PATCH 01/11] Added 'pip install build' to development instructions, otherwise python -m build won't work --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4ee7998..4620a25 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ If you'd like to modify the code and contribute to development, you can [downloa ```bash # Go to the directory where you extracted the repository and run: pip install -e . # Installs the package in editable mode +pip install build # Install the build package python -m build # Builds the package in dist folder (optional) abogen # Opens the GUI ``` From 0d86d107e9c402b7c261f60510dc98b7a2a4f9c0 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Fri, 2 May 2025 18:50:33 +0200 Subject: [PATCH 02/11] remove the animation from preview button on cleanup --- abogen/gui.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/abogen/gui.py b/abogen/gui.py index 3f7ceec..1b980bc 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1545,7 +1545,11 @@ class abogen(QWidget): def _preview_cleanup(self): self.preview_playing = False - self.btn_preview.setIcon(self.play_icon) + try: + self.loading_movie.frameChanged.disconnect() + except Exception: + pass # Ignore error if not connected + self.btn_preview.setIcon(self.play_icon) self.btn_preview.setToolTip("Preview selected voice") self.btn_preview.setEnabled(True) self.voice_combo.setEnabled(True) From 036eed31421dd42b2abb517f14de0c4bd955d4f1 Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Fri, 2 May 2025 18:58:38 +0200 Subject: [PATCH 03/11] also stopping the loading movie on preview cleanup --- abogen/gui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/abogen/gui.py b/abogen/gui.py index 1b980bc..3ab1a3c 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1545,6 +1545,7 @@ class abogen(QWidget): def _preview_cleanup(self): self.preview_playing = False + self.loading_movie.stop() try: self.loading_movie.frameChanged.disconnect() except Exception: From e7d479d8466db9709dfb5216a5132891986dd3bb Mon Sep 17 00:00:00 2001 From: Juraj Borza Date: Fri, 2 May 2025 19:00:01 +0200 Subject: [PATCH 04/11] use CPU conversion if GPU conversion is not available also in preview --- abogen/gui.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/abogen/gui.py b/abogen/gui.py index 3ab1a3c..5288a3d 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1480,8 +1480,11 @@ class abogen(QWidget): lang = self.selected_voice[0] voice = self.selected_voice + # use same gpu/cpu logic as in conversion + gpu_msg, gpu_ok = get_gpu_acceleration(self.use_gpu) + self.preview_thread = VoicePreviewThread( - np_module, kpipeline_class, lang, voice, speed, self.use_gpu + np_module, kpipeline_class, lang, voice, speed, gpu_ok ) self.preview_thread.finished.connect(self._play_preview_audio) self.preview_thread.error.connect(self._preview_error) From 07a2fe2d5ba8606a23730246428729c8bdc4e0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Fri, 2 May 2025 20:59:23 +0300 Subject: [PATCH 05/11] Fix new profile duplicating --- abogen/voice_formula_gui.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py index 75aeebf..d26206f 100644 --- a/abogen/voice_formula_gui.py +++ b/abogen/voice_formula_gui.py @@ -1192,8 +1192,8 @@ class VoiceFormulaDialog(QDialog): def rename_profile(self, item): name = item.text().lstrip("*") - # block if profile has unsaved changes - if self._profile_dirty.get(name, False): + # block if profile has unsaved changes and it's not a virtual New profile + if self._profile_dirty.get(name, False) and not (self._virtual_new_profile and name == "New profile"): QMessageBox.warning( self, "Unsaved Changes", "Please save the profile before renaming." ) @@ -1212,13 +1212,39 @@ class VoiceFormulaDialog(QDialog): if not re.match(r'^[\w\- ]+$', new): QMessageBox.warning(self, "Invalid Name", "Profile name can only contain letters, numbers, spaces, underscores, and hyphens.") continue + profiles = load_profiles() if new in profiles: QMessageBox.warning(self, "Duplicate Name", "Profile already exists.") continue - profiles[new] = profiles.pop(old) - save_profiles(profiles) - item.setText(new) + + # Special case for renaming the virtual "New profile" + if self._virtual_new_profile and name == "New profile": + # Create the profile with the new name + profiles[new] = { + "voices": self.get_selected_voices(), + "language": self.language_combo.currentData(), + } + save_profiles(profiles) + + # Update tracking properties + self._virtual_new_profile = False + self._profile_dirty.pop("New profile", None) + self._profile_dirty[new] = False + + # Update the current profile name + self.current_profile = new + item.setText(new) + else: + # Standard renaming for regular profiles + profiles[new] = profiles.pop(old) + save_profiles(profiles) + item.setText(new) + + # Update the current profile name if it was renamed + if self.current_profile == old: + self.current_profile = new + parent = self.parent() if hasattr(parent, "populate_profiles_in_voice_combo"): parent.populate_profiles_in_voice_combo() From d5388c62be701512b1cf3ac946d446b3844c2586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Fri, 2 May 2025 21:16:51 +0300 Subject: [PATCH 06/11] Fix enable/disable subtitle_combo fom voice mixer --- abogen/voice_formula_gui.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/abogen/voice_formula_gui.py b/abogen/voice_formula_gui.py index d26206f..50da22b 100644 --- a/abogen/voice_formula_gui.py +++ b/abogen/voice_formula_gui.py @@ -475,7 +475,6 @@ class VoiceFormulaDialog(QDialog): self.add_voices(initial_state or []) self.update_weighted_sums() - self.update_subtitle_combo_enabled() # assemble splitter splitter.addWidget(profile_widget) @@ -496,8 +495,6 @@ class VoiceFormulaDialog(QDialog): for vm in self.voice_mixers: vm.spin_box.valueChanged.connect(self.mark_profile_modified) vm.checkbox.stateChanged.connect(lambda *_: self.mark_profile_modified()) - vm.spin_box.valueChanged.connect(self.update_subtitle_combo_enabled) - vm.checkbox.stateChanged.connect(self.update_subtitle_combo_enabled) def keyPressEvent(self, event): # Bind Delete key to delete_profile when a profile is selected @@ -682,8 +679,6 @@ class VoiceFormulaDialog(QDialog): voice_mixer.checkbox.stateChanged.connect( lambda *_: self.mark_profile_modified() ) - voice_mixer.spin_box.valueChanged.connect(self.update_subtitle_combo_enabled) - voice_mixer.checkbox.stateChanged.connect(self.update_subtitle_combo_enabled) return voice_mixer def handle_voice_checkbox(self, voice_mixer, state): @@ -743,20 +738,6 @@ class VoiceFormulaDialog(QDialog): self.error_label.show() self.weighted_sums_container.hide() - def update_subtitle_combo_enabled(self): - # Only enable subtitle_combo if at least one selected voice is from supported languages - selected_langs = set() - for vm in self.voice_mixers: - if vm.checkbox.isChecked() and vm.spin_box.value() > 0: - lang_code = vm.voice_name[0] - selected_langs.add(lang_code) - enable = any( - lang in SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION - for lang in selected_langs - ) - if self.subtitle_combo: - self.subtitle_combo.setEnabled(enable) - def disable_voice_by_name(self, voice_name): for mixer in self.voice_mixers: if mixer.voice_name == voice_name: @@ -819,7 +800,6 @@ class VoiceFormulaDialog(QDialog): # sync enabled state vm.toggle_inputs() self.update_weighted_sums() - self.update_subtitle_combo_enabled() def save_profile_by_name(self, name): profiles = load_profiles() From 20f31f82fd59607482f1c677d391864039641d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Fri, 2 May 2025 21:18:07 +0300 Subject: [PATCH 07/11] v1.0.4 --- CHANGELOG.md | 2 ++ abogen/VERSION | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85645c4..28eb291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ +- Merge pull request [#7](https://github.com/denizsafak/abogen/pull/7) by [@jborza](https://github.com/jborza) that improves voice preview and documentation. - Fixed the issue when a voice is selected, the voice mixer tries to pre-select that voice and ignores existing profiles. - Fixed the error while renaming the default "New profile" in the voice mixer. +- Fixed subtitle_combo enabling/disabling when a voice in the voice mixer is selected. - Prevented using special characters in the profile name to avoid conflicts. - Improved invalid profile handling in the voice mixer. \ No newline at end of file diff --git a/abogen/VERSION b/abogen/VERSION index e4c0d46..a6a3a43 100644 --- a/abogen/VERSION +++ b/abogen/VERSION @@ -1 +1 @@ -1.0.3 \ No newline at end of file +1.0.4 \ No newline at end of file From 8daa3cb3b55ae4cd1006896dba1cf1f0738e69b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Sat, 3 May 2025 12:09:26 +0300 Subject: [PATCH 08/11] Update readme about chapter markers --- README.md | 26 +++++++++++++++++++++++++- demo/chapter_marker.png | Bin 0 -> 11208 bytes 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 demo/chapter_marker.png diff --git a/README.md b/README.md index 4620a25..0b1e6d0 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex - **Chapter Control**: Select specific `chapters` from ePUBs or `chapters + pages` from PDFs. - **Options**: - **Replace single newlines with spaces**: Replaces single newlines with spaces in the text. This is useful for texts that have imaginary line breaks. - - **Configure max words per subtitle**: Automatically configures the maximum number of words per subtitle entry. + - **Configure max words per subtitle**: Configures the maximum number of words per subtitle entry. - **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. @@ -103,6 +103,30 @@ Here’s Abogen in action: in this demo, it processes ∼3,000 characters of tex With voice mixer, you can create custom voices by mixing different voice models. You can adjust the weight of each voice and save your custom voice as a profile for future use. The voice mixer allows you to create unique and personalized voices. (Huge thanks to [@jborza](https://github.com/jborza) for making this possible through his contributions in [#5](https://github.com/denizsafak/abogen/pull/5)) +## `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: + +``` + +``` +These are chapter markers. They are automatically added when you process ePUB or PDF files, based on the chapters you select. They serve an important purpose: +- Allow you to split the text into separate audio files for each chapter +- Save time by letting you reprocess only specific chapters if errors occur, rather than the entire file + +You can manually add these markers to plain text files for the same benefits. Simply include them in your text like this: + +``` + +This is the beginning of my text... + + +Here's another part... +``` +When you process the text file, abogen will detect these markers automatically and ask if you want to save each chapter separately and create a merged version. + +![Abogen Chapter Marker](https://raw.githubusercontent.com/denizsafak/abogen/refs/heads/main/demo/chapter_marker.png) + + ## `Supported Languages` ``` # 🇺🇸 'a' => American English, 🇬🇧 'b' => British English diff --git a/demo/chapter_marker.png b/demo/chapter_marker.png new file mode 100644 index 0000000000000000000000000000000000000000..5e676fb9539561cbb1bc140916dfcaa0a0769321 GIT binary patch literal 11208 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 Date: Sat, 3 May 2025 16:00:12 +0300 Subject: [PATCH 09/11] Fix loading movie not playing again after first time --- abogen/gui.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/abogen/gui.py b/abogen/gui.py index f7be737..ad1cb6f 100644 --- a/abogen/gui.py +++ b/abogen/gui.py @@ -1417,7 +1417,6 @@ class abogen(QWidget): if self.preview_playing: try: import pygame - pygame.mixer.music.stop() except Exception: pass @@ -1432,8 +1431,20 @@ class abogen(QWidget): self.voice_combo.setEnabled(False) self.btn_voice_formula_mixer.setEnabled(False) # Disable mixer button self.btn_start.setEnabled(False) # Disable start button during preview - # start loading animation - self.loading_movie.start() + + # Start loading animation - ensure signal connection is always active + if hasattr(self, 'loading_movie'): + # Disconnect previous connections to avoid multiple connections + try: + self.loading_movie.frameChanged.disconnect() + except TypeError: + pass # Ignore error if not connected + + # Reconnect the signal + self.loading_movie.frameChanged.connect( + lambda: self.btn_preview.setIcon(QIcon(self.loading_movie.currentPixmap())) + ) + self.loading_movie.start() def pipeline_loaded_callback(np_module, kpipeline_class, error): self._on_pipeline_loaded_for_preview(np_module, kpipeline_class, error) From 0863b25d82104c9d598a1e598a348d7e72344d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Sat, 3 May 2025 18:31:45 +0300 Subject: [PATCH 10/11] Better approach for detemining the correct configuration folder for Linux and MacOS, using platformdirs --- CHANGELOG.md | 7 +------ abogen/utils.py | 7 +++---- abogen/voice_profiles.py | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28eb291..d3d73b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1 @@ -- Merge pull request [#7](https://github.com/denizsafak/abogen/pull/7) by [@jborza](https://github.com/jborza) that improves voice preview and documentation. -- Fixed the issue when a voice is selected, the voice mixer tries to pre-select that voice and ignores existing profiles. -- Fixed the error while renaming the default "New profile" in the voice mixer. -- Fixed subtitle_combo enabling/disabling when a voice in the voice mixer is selected. -- Prevented using special characters in the profile name to avoid conflicts. -- Improved invalid profile handling in the voice mixer. \ No newline at end of file +- Better approach for detemining the correct configuration folder for Linux and MacOS, using platformdirs. \ No newline at end of file diff --git a/abogen/utils.py b/abogen/utils.py index f65bc8e..d8cd5cd 100644 --- a/abogen/utils.py +++ b/abogen/utils.py @@ -66,10 +66,9 @@ def get_version(): # Define config path def get_user_config_path(): - if os.name == "nt": - config_dir = os.path.join(os.environ["APPDATA"], "abogen") - else: - config_dir = os.path.join(os.path.expanduser("~"), ".config", "abogen") + from platformdirs import user_config_dir + # Use platformdirs to get the user configuration directory + config_dir = user_config_dir("abogen", appauthor=False, roaming=True) os.makedirs(config_dir, exist_ok=True) return os.path.join(config_dir, "config.json") diff --git a/abogen/voice_profiles.py b/abogen/voice_profiles.py index a77a98a..637ecce 100644 --- a/abogen/voice_profiles.py +++ b/abogen/voice_profiles.py @@ -1,6 +1,6 @@ import os import json -from utils import get_user_config_path, get_resource_path +from utils import get_user_config_path def _get_profiles_path(): From 6bf189312af2c8ba49667728d271cdf54c341ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Sat, 3 May 2025 22:55:41 +0300 Subject: [PATCH 11/11] Add docker support, fix config dir --- README.md | 32 ++++++++++++++++++++++++++++++ abogen/Dockerfile | 50 +++++++++++++++++++++++++++++++++++++++++++++++ abogen/utils.py | 14 +++++++++++-- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 abogen/Dockerfile diff --git a/README.md b/README.md index 0b1e6d0..93493cb 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,38 @@ audio-samplerate=48000 volume-max=200 ``` +## `Docker Guide` +If you want to run Abogen in a Docker container: +1) [Download the repository](https://github.com/denizsafak/abogen/archive/refs/heads/main.zip) and extract, or clone it using git. +2) Go to `abogen` folder. You should see `Dockerfile` there. +3) Open your termminal in that directory and run the following commands: + +```bash +# Build the Docker image: +docker build --progress plain -t abogen . + +# Note that building the image may take a while. +# After building is complete, run the Docker container: + +# Windows +docker run --name abogen -v %cd%:/shared -p 5800:5800 -p 5900:5900 --gpus all abogen + +# Linux +docker run --name abogen -v $(pwd):/shared -p 5800:5800 -p 5900:5900 --gpus all abogen + +# MacOS +docker run --name abogen -v $(pwd):/shared -p 5800:5800 -p 5900:5900 abogen + +# We expose port 5800 for use by a web browser, 5900 if you want to connect with a VNC client. +``` + +Abogen launches automatically inside the container. +- You can access it via a web browser at `http://localhost:5800` or connect to it using a VNC client at `localhost:5900`. +- You can use `/shared` directory to share files between your host and the container. +- For later use, start it with `docker start abogen` and stop it with `docker stop 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/).) + ## `Similar Projects` Abogen is a standalone project, but it is inspired by and shares some similarities with other projects. Here are a few: - [audiblez](https://github.com/santinic/audiblez): Generate audiobooks from e-books. **(Has CLI and GUI support)** diff --git a/abogen/Dockerfile b/abogen/Dockerfile new file mode 100644 index 0000000..a4fd02a --- /dev/null +++ b/abogen/Dockerfile @@ -0,0 +1,50 @@ +# Special thanks to @geo38 from Reddit, who provided this Dockerfile: +# https://www.reddit.com/r/selfhosted/comments/1k8x1yo/comment/mpe0bz8/ + +# Use a docker base image that runs a window manager that can be viewed +# outside the image with a web browser or VNC client. +# https://github.com/jlesage/docker-baseimage-gui + +FROM jlesage/baseimage-gui:debian-12-v4 + +# Load stuff needed by abogen +RUN apt-get update \ + && apt-get install -y \ + python3 \ + python3-venv \ + python3-pip \ + python3-pyqt5 \ + espeak-ng \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# The base image will run /startapp.sh on launch. +# +# The base image runs that script as user 'app' uid=1000. That user +# does not exist in the base image but is created at run time. +# +# We need to install abogen in python venv (requirement of newer python3). +# +# The python venv has to be writable by the 'app' user as abogen dynamically +# installs python packages, so create the venv as that user +# +# We intend to share the /shared directory with the host using a bind volume +# in order to access any source files and the created files. + +RUN echo '#!/bin/bash\nsource /app/venv/bin/activate\nexec abogen' > /startapp.sh \ + && chmod 555 /startapp.sh \ + && mkdir /app /shared \ + && chown 1000:1000 /app /shared \ + && chmod 755 /app /shared + +# Switch to user 1000 to create the virtual environment +USER 1000:1000 +RUN python3 -m venv /app/venv + +# Make /app the working dir, copy your project in, and install from “.” +COPY . /app/ +WORKDIR /app +RUN /bin/bash -c "source venv/bin/activate && pip install ." + +# Change back to root for the base‐image startup scripts +USER root \ No newline at end of file diff --git a/abogen/utils.py b/abogen/utils.py index d8cd5cd..1b0625b 100644 --- a/abogen/utils.py +++ b/abogen/utils.py @@ -67,8 +67,18 @@ def get_version(): # Define config path def get_user_config_path(): from platformdirs import user_config_dir - # Use platformdirs to get the user configuration directory - config_dir = user_config_dir("abogen", appauthor=False, roaming=True) + # TODO Config directory is changed for Linux and MacOS. But if old config exists, it will be used. + # On non‑Windows, prefer ~/.config/abogen if it already exists + if platform.system() != "Windows": + custom_dir = os.path.join(os.path.expanduser("~"), ".config", "abogen") + if os.path.exists(custom_dir): + config_dir = custom_dir + else: + config_dir = user_config_dir("abogen", appauthor=False, roaming=True) + else: + # Windows and fallback case + config_dir = user_config_dir("abogen", appauthor=False, roaming=True) + os.makedirs(config_dir, exist_ok=True) return os.path.join(config_dir, "config.json")