From 0f5003dfddec7a1030b18baa52b9f7e3aed06374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 01:37:50 +0300 Subject: [PATCH 01/12] fix: add missing imports for get_resource_path and load_integration_settings --- abogen/pyqt/main.py | 2 ++ abogen/webui/routes/settings.py | 1 + 2 files changed, 3 insertions(+) diff --git a/abogen/pyqt/main.py b/abogen/pyqt/main.py index 9e11c0c..8371899 100644 --- a/abogen/pyqt/main.py +++ b/abogen/pyqt/main.py @@ -46,6 +46,8 @@ except ImportError: print("PyQt6 not installed.") +from abogen.utils import get_resource_path + # Pre-load "libxcb-cursor" on Linux (fixes #101) if platform.system() == "Linux": arch = platform.machine().lower() diff --git a/abogen/webui/routes/settings.py b/abogen/webui/routes/settings.py index ba4ea07..975cad4 100644 --- a/abogen/webui/routes/settings.py +++ b/abogen/webui/routes/settings.py @@ -7,6 +7,7 @@ from flask import Blueprint, current_app, render_template, request, redirect, ur from flask.typing import ResponseReturnValue from abogen.webui.routes.utils.settings import ( + load_integration_settings, load_settings, save_settings, SAVE_MODE_LABELS, From 473631b84e8abbe321aa49bd63c8c49c509fa40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 01:41:13 +0300 Subject: [PATCH 02/12] fix: use theme-aware GREY_BACKGROUND for word substitutions instructions label --- abogen/pyqt/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abogen/pyqt/gui.py b/abogen/pyqt/gui.py index 6120a34..fca8c0f 100644 --- a/abogen/pyqt/gui.py +++ b/abogen/pyqt/gui.py @@ -842,7 +842,7 @@ class WordSubstitutionsDialog(QDialog): self, ) instructions.setStyleSheet( - "padding: 10px; background-color: #f0f0f0; border-radius: 5px;" + f"padding: 10px; background-color: {COLORS['GREY_BACKGROUND']}; border-radius: 5px;" ) instructions.setWordWrap(True) layout.addWidget(instructions) From a0fdabd81f1ebf9085c372c36d5fbe6e5a12b845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 01:42:41 +0300 Subject: [PATCH 03/12] fix: suppress harmless Qt portal registration warning on Linux --- abogen/pyqt/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/abogen/pyqt/main.py b/abogen/pyqt/main.py index 8371899..437068a 100644 --- a/abogen/pyqt/main.py +++ b/abogen/pyqt/main.py @@ -120,6 +120,8 @@ def qt_message_handler(mode, context, message): return # Suppress this specific message if "setGrabPopup called with a parent, QtWaylandClient" in message: return + if "Failed to register with host portal" in message: + return if mode == QtMsgType.QtWarningMsg: print(f"Qt Warning: {message}") From 14913b45e9208d2b7b7eb917ca88ce4c2d8bd000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 02:17:46 +0300 Subject: [PATCH 04/12] =?UTF-8?q?fix:=20import=20importlib.util=20explicit?= =?UTF-8?q?ly=20(not=20auto-loaded=20in=20Python=203.12)=20=E2=80=94=20bro?= =?UTF-8?q?ke=20plugin=20loading,=20causing=20empty=20voice=20lists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- abogen/tts_plugin/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abogen/tts_plugin/loader.py b/abogen/tts_plugin/loader.py index fa600ab..7c8baf1 100644 --- a/abogen/tts_plugin/loader.py +++ b/abogen/tts_plugin/loader.py @@ -12,7 +12,7 @@ The loader does NOT: from __future__ import annotations -import importlib +import importlib.util import re import sys import types From ec55918b0453807b1d70750fce1552a136371b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 02:28:41 +0300 Subject: [PATCH 05/12] fix: add load_single_voice to Pipeline wrapper to prevent formula string being used as download filename --- abogen/tts_plugin/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/abogen/tts_plugin/utils.py b/abogen/tts_plugin/utils.py index 0bbdb71..dd92431 100644 --- a/abogen/tts_plugin/utils.py +++ b/abogen/tts_plugin/utils.py @@ -179,6 +179,12 @@ class Pipeline: yield Segment(graphemes=text, audio=audio_array) + def load_single_voice(self, voice_name: str) -> Any: + engine_pipeline = getattr(self._engine, '_pipeline', None) + if engine_pipeline is not None and hasattr(engine_pipeline, 'load_single_voice'): + return engine_pipeline.load_single_voice(voice_name) + raise AttributeError(f"load_single_voice not available on {type(self._engine).__name__}") + def dispose(self) -> None: if self._session is not None: try: From d5cddb974986a5bd0cc132b8b27a135877b95ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 02:33:18 +0300 Subject: [PATCH 06/12] fix: pass mock job object to merge_pronunciation_overrides instead of positional args --- abogen/pyqt/conversion.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index 7632923..6b3af4f 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -550,10 +550,14 @@ class ConversionThread(QThread): # --- Compile normalization rules (heteronym + pronunciation) --- from abogen.domain.normalization import TTSContext - pronunciation_overrides = merge_pronunciation_overrides( - getattr(self, "pronunciation_overrides", None), - getattr(self, "manual_overrides", None), - ) + + class _MergeJob: + pronunciation_overrides = getattr(self, "pronunciation_overrides", None) + manual_overrides = getattr(self, "manual_overrides", None) + heteronym_overrides = getattr(self, "heteronym_overrides", None) + language = self.lang_code + + pronunciation_overrides = merge_pronunciation_overrides(_MergeJob()) self._tts_context = TTSContext( split_pattern=self.split_pattern, pronunciation_rules=compile_pronunciation_rules(pronunciation_overrides), From bd99ee1ba1377fa072f4ce172c870eeddde454a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 03:15:21 +0300 Subject: [PATCH 07/12] fix: subtitle FakeToken split, missing run_tts_segment_loop import - subtitle_generation: split multi-sentence FakeToken into separate entries - conversion.py: add missing run_tts_segment_loop import No changes to spacy_utils or Language enum. --- abogen/domain/subtitle_generation.py | 18 +++++++++++++++--- abogen/pyqt/conversion.py | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/abogen/domain/subtitle_generation.py b/abogen/domain/subtitle_generation.py index 1428f71..214aade 100644 --- a/abogen/domain/subtitle_generation.py +++ b/abogen/domain/subtitle_generation.py @@ -280,16 +280,28 @@ def _process_regex_sentences( current_sentence = [] word_count = 0 - # Add any remaining tokens as a sentence + # Add any remaining tokens as a sentence (split multi-sentence FakeToken) if current_sentence: start_time = current_sentence[0]["start"] end_time = current_sentence[-1]["end"] - # Simplified text joining logic sentence_text = "" for t in current_sentence: sentence_text += t["text"] + (t.get("whitespace") or "") - subtitle_entries.append((start_time, end_time, sentence_text.strip())) + sentence_text = sentence_text.strip() + + if len(current_sentence) == 1: + parts = re.split(rf"(?<={separator})\s+", sentence_text) + if len(parts) > 1: + d = end_time - start_time + for i, p in enumerate(parts): + e = end_time if i == len(parts) - 1 else start_time + d * len(p) / len(sentence_text) + subtitle_entries.append((start_time, e, p.strip())) + start_time = e + current_sentence = [] + + if current_sentence: + subtitle_entries.append((start_time, end_time, sentence_text)) # Fallback for last entry _apply_fallback_end_time(subtitle_entries, fallback_end_time) diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index 6b3af4f..72e545d 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -36,7 +36,7 @@ from abogen.domain.output_paths import ( ) from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32 from abogen.domain.audio_sink import AudioSink, open_audio_sink -from abogen.domain.conversion_engine import synthesize_text, SynthParams, SegmentStats, SegmentInfo +from abogen.domain.conversion_engine import run_tts_segment_loop, synthesize_text, SynthParams, SegmentStats, SegmentInfo from abogen.domain.intro_outro import resolve_intro, resolve_outro from abogen.domain.audio_buffer import ( create_silence, From dbcbb1c8a92714512888be0c5165bd3ca5bbcd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 03:18:51 +0300 Subject: [PATCH 08/12] Fix NameError: add missing 'from pathlib import Path' in pyqt/conversion.py --- abogen/pyqt/conversion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/abogen/pyqt/conversion.py b/abogen/pyqt/conversion.py index 72e545d..aa92801 100644 --- a/abogen/pyqt/conversion.py +++ b/abogen/pyqt/conversion.py @@ -2,6 +2,7 @@ import os import re import time import hashlib # For generating unique cache filenames +from pathlib import Path from platformdirs import user_desktop_dir from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer from PyQt6.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox From 27f88b759db490847e1fba31ca1cc33aac7e9f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 03:26:58 +0300 Subject: [PATCH 09/12] Fix spurious HF HEAD requests: return early on cache hit in tracked_hf_hub_download --- abogen/hf_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abogen/hf_tracker.py b/abogen/hf_tracker.py index 84c6350..73ae968 100644 --- a/abogen/hf_tracker.py +++ b/abogen/hf_tracker.py @@ -19,7 +19,7 @@ def tracked_hf_hub_download(*args, **kwargs): try: local_kwargs = dict(kwargs) local_kwargs["local_files_only"] = True - hf_hub_download(*args, **local_kwargs) + return hf_hub_download(*args, **local_kwargs) except Exception: repo_id = kwargs.get("repo_id", "") filename = kwargs.get("filename", "") From 342ea0dfacbdf61834f430abc935072dd6e9e6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 03:33:06 +0300 Subject: [PATCH 10/12] Fix spaCy unknown language error: map Kokoro single-letter codes to Language enum in get_spacy_model --- abogen/spacy_utils.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/abogen/spacy_utils.py b/abogen/spacy_utils.py index d03e859..d2f331a 100644 --- a/abogen/spacy_utils.py +++ b/abogen/spacy_utils.py @@ -21,6 +21,19 @@ SPACY_MODELS = { Language.HI: "xx_sent_ud_sm", } +# Kokoro single-letter codes -> Language enum (inverse of pipeline_factory._KOKORO_LANG_MAP) +_KOKORO_TO_LANGUAGE = { + "a": Language.EN_US, + "b": Language.EN_GB, + "e": Language.ES, + "f": Language.FR, + "h": Language.HI, + "i": Language.IT, + "j": Language.JA, + "p": Language.PT_BR, + "z": Language.ZH, +} + def _load_spacy(): """Lazy load spaCy module.""" @@ -61,11 +74,14 @@ def get_spacy_model(lang_code, log_callback=None): # Normalize to Language enum if not isinstance(lang_code, Language): - try: - lang_code = Language.from_str(lang_code) - except ValueError: - log(f"\nspaCy: Unknown language '{lang_code}'...") - return None + if isinstance(lang_code, str) and lang_code in _KOKORO_TO_LANGUAGE: + lang_code = _KOKORO_TO_LANGUAGE[lang_code] + else: + try: + lang_code = Language.from_str(lang_code) + except ValueError: + log(f"\nspaCy: Unknown language '{lang_code}'...") + return None # Check if model is cached if lang_code in _nlp_cache: From 6274a02d5e38d9cbeaf59bdf71809791b92c99cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 03:55:07 +0300 Subject: [PATCH 11/12] Fix empty voice list when launched via desktop shortcut PluginManager.discover() used a relative path 'plugins', which resolved against the CWD. When launched from a desktop shortcut the CWD is ~, so the plugins directory was never found and no voices appeared in the list. Fall back to the project-relative plugins path when the default relative path doesn't resolve. --- abogen/tts_plugin/plugin_manager.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/abogen/tts_plugin/plugin_manager.py b/abogen/tts_plugin/plugin_manager.py index 1589d08..6789620 100644 --- a/abogen/tts_plugin/plugin_manager.py +++ b/abogen/tts_plugin/plugin_manager.py @@ -42,8 +42,11 @@ class PluginManager: plugins_path = Path(plugins_dir) if not plugins_path.exists(): - self._loaded = True - return + if plugins_dir == "plugins": + plugins_path = Path(__file__).resolve().parent.parent.parent / "plugins" + if not plugins_path.exists(): + self._loaded = True + return for entry in plugins_path.iterdir(): if entry.is_dir() and (entry / "__init__.py").exists(): From 9201f58770180aece0c4c96a681a7342aee117ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C5=9Eafak?= Date: Thu, 23 Jul 2026 16:27:54 +0300 Subject: [PATCH 12/12] Add k0sm0naft to GitHub funding list --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index a6510f0..e25402f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: [jborza, jeremiahsb, mohangk] +github: [jborza, jeremiahsb, mohangk, k0sm0naft] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username