Compare commits

...
11 Commits
Author SHA1 Message Date
Deniz Şafak 6274a02d5e 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.
2026-07-23 03:55:07 +03:00
Deniz Şafak 342ea0dfac Fix spaCy unknown language error: map Kokoro single-letter codes to Language enum in get_spacy_model 2026-07-23 03:33:06 +03:00
Deniz Şafak 27f88b759d Fix spurious HF HEAD requests: return early on cache hit in tracked_hf_hub_download 2026-07-23 03:26:58 +03:00
Deniz Şafak dbcbb1c8a9 Fix NameError: add missing 'from pathlib import Path' in pyqt/conversion.py 2026-07-23 03:18:51 +03:00
Deniz Şafak bd99ee1ba1 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.
2026-07-23 03:15:21 +03:00
Deniz Şafak d5cddb9749 fix: pass mock job object to merge_pronunciation_overrides instead of positional args 2026-07-23 02:33:18 +03:00
Deniz Şafak ec55918b04 fix: add load_single_voice to Pipeline wrapper to prevent formula string being used as download filename 2026-07-23 02:28:41 +03:00
Deniz Şafak 14913b45e9 fix: import importlib.util explicitly (not auto-loaded in Python 3.12) — broke plugin loading, causing empty voice lists 2026-07-23 02:17:46 +03:00
Deniz Şafak a0fdabd81f fix: suppress harmless Qt portal registration warning on Linux 2026-07-23 01:42:41 +03:00
Deniz Şafak 473631b84e fix: use theme-aware GREY_BACKGROUND for word substitutions instructions label 2026-07-23 01:41:13 +03:00
Deniz Şafak 0f5003dfdd fix: add missing imports for get_resource_path and load_integration_settings 2026-07-23 01:37:50 +03:00
10 changed files with 65 additions and 18 deletions
+15 -3
View File
@@ -280,16 +280,28 @@ def _process_regex_sentences(
current_sentence = [] current_sentence = []
word_count = 0 word_count = 0
# Add any remaining tokens as a sentence # Add any remaining tokens as a sentence (split multi-sentence FakeToken)
if current_sentence: if current_sentence:
start_time = current_sentence[0]["start"] start_time = current_sentence[0]["start"]
end_time = current_sentence[-1]["end"] end_time = current_sentence[-1]["end"]
# Simplified text joining logic
sentence_text = "" sentence_text = ""
for t in current_sentence: for t in current_sentence:
sentence_text += t["text"] + (t.get("whitespace") or "") 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 # Fallback for last entry
_apply_fallback_end_time(subtitle_entries, fallback_end_time) _apply_fallback_end_time(subtitle_entries, fallback_end_time)
+1 -1
View File
@@ -19,7 +19,7 @@ def tracked_hf_hub_download(*args, **kwargs):
try: try:
local_kwargs = dict(kwargs) local_kwargs = dict(kwargs)
local_kwargs["local_files_only"] = True local_kwargs["local_files_only"] = True
hf_hub_download(*args, **local_kwargs) return hf_hub_download(*args, **local_kwargs)
except Exception: except Exception:
repo_id = kwargs.get("repo_id", "<unknown repo>") repo_id = kwargs.get("repo_id", "<unknown repo>")
filename = kwargs.get("filename", "<unknown file>") filename = kwargs.get("filename", "<unknown file>")
+10 -5
View File
@@ -2,6 +2,7 @@ import os
import re import re
import time import time
import hashlib # For generating unique cache filenames import hashlib # For generating unique cache filenames
from pathlib import Path
from platformdirs import user_desktop_dir from platformdirs import user_desktop_dir
from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer from PyQt6.QtCore import QThread, pyqtSignal, Qt, QTimer
from PyQt6.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox from PyQt6.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
@@ -36,7 +37,7 @@ from abogen.domain.output_paths import (
) )
from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32 from abogen.domain.audio_helpers import build_ffmpeg_command, to_float32
from abogen.domain.audio_sink import AudioSink, open_audio_sink 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.intro_outro import resolve_intro, resolve_outro
from abogen.domain.audio_buffer import ( from abogen.domain.audio_buffer import (
create_silence, create_silence,
@@ -550,10 +551,14 @@ class ConversionThread(QThread):
# --- Compile normalization rules (heteronym + pronunciation) --- # --- Compile normalization rules (heteronym + pronunciation) ---
from abogen.domain.normalization import TTSContext from abogen.domain.normalization import TTSContext
pronunciation_overrides = merge_pronunciation_overrides(
getattr(self, "pronunciation_overrides", None), class _MergeJob:
getattr(self, "manual_overrides", None), 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( self._tts_context = TTSContext(
split_pattern=self.split_pattern, split_pattern=self.split_pattern,
pronunciation_rules=compile_pronunciation_rules(pronunciation_overrides), pronunciation_rules=compile_pronunciation_rules(pronunciation_overrides),
+1 -1
View File
@@ -842,7 +842,7 @@ class WordSubstitutionsDialog(QDialog):
self, self,
) )
instructions.setStyleSheet( instructions.setStyleSheet(
"padding: 10px; background-color: #f0f0f0; border-radius: 5px;" f"padding: 10px; background-color: {COLORS['GREY_BACKGROUND']}; border-radius: 5px;"
) )
instructions.setWordWrap(True) instructions.setWordWrap(True)
layout.addWidget(instructions) layout.addWidget(instructions)
+4
View File
@@ -46,6 +46,8 @@ except ImportError:
print("PyQt6 not installed.") print("PyQt6 not installed.")
from abogen.utils import get_resource_path
# Pre-load "libxcb-cursor" on Linux (fixes #101) # Pre-load "libxcb-cursor" on Linux (fixes #101)
if platform.system() == "Linux": if platform.system() == "Linux":
arch = platform.machine().lower() arch = platform.machine().lower()
@@ -118,6 +120,8 @@ def qt_message_handler(mode, context, message):
return # Suppress this specific message return # Suppress this specific message
if "setGrabPopup called with a parent, QtWaylandClient" in message: if "setGrabPopup called with a parent, QtWaylandClient" in message:
return return
if "Failed to register with host portal" in message:
return
if mode == QtMsgType.QtWarningMsg: if mode == QtMsgType.QtWarningMsg:
print(f"Qt Warning: {message}") print(f"Qt Warning: {message}")
+21 -5
View File
@@ -21,6 +21,19 @@ SPACY_MODELS = {
Language.HI: "xx_sent_ud_sm", 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(): def _load_spacy():
"""Lazy load spaCy module.""" """Lazy load spaCy module."""
@@ -61,11 +74,14 @@ def get_spacy_model(lang_code, log_callback=None):
# Normalize to Language enum # Normalize to Language enum
if not isinstance(lang_code, Language): if not isinstance(lang_code, Language):
try: if isinstance(lang_code, str) and lang_code in _KOKORO_TO_LANGUAGE:
lang_code = Language.from_str(lang_code) lang_code = _KOKORO_TO_LANGUAGE[lang_code]
except ValueError: else:
log(f"\nspaCy: Unknown language '{lang_code}'...") try:
return None lang_code = Language.from_str(lang_code)
except ValueError:
log(f"\nspaCy: Unknown language '{lang_code}'...")
return None
# Check if model is cached # Check if model is cached
if lang_code in _nlp_cache: if lang_code in _nlp_cache:
+1 -1
View File
@@ -12,7 +12,7 @@ The loader does NOT:
from __future__ import annotations from __future__ import annotations
import importlib import importlib.util
import re import re
import sys import sys
import types import types
+5 -2
View File
@@ -42,8 +42,11 @@ class PluginManager:
plugins_path = Path(plugins_dir) plugins_path = Path(plugins_dir)
if not plugins_path.exists(): if not plugins_path.exists():
self._loaded = True if plugins_dir == "plugins":
return plugins_path = Path(__file__).resolve().parent.parent.parent / "plugins"
if not plugins_path.exists():
self._loaded = True
return
for entry in plugins_path.iterdir(): for entry in plugins_path.iterdir():
if entry.is_dir() and (entry / "__init__.py").exists(): if entry.is_dir() and (entry / "__init__.py").exists():
+6
View File
@@ -179,6 +179,12 @@ class Pipeline:
yield Segment(graphemes=text, audio=audio_array) 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: def dispose(self) -> None:
if self._session is not None: if self._session is not None:
try: try:
+1
View File
@@ -7,6 +7,7 @@ from flask import Blueprint, current_app, render_template, request, redirect, ur
from flask.typing import ResponseReturnValue from flask.typing import ResponseReturnValue
from abogen.webui.routes.utils.settings import ( from abogen.webui.routes.utils.settings import (
load_integration_settings,
load_settings, load_settings,
save_settings, save_settings,
SAVE_MODE_LABELS, SAVE_MODE_LABELS,