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 = []
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)
+1 -1
View File
@@ -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", "<unknown repo>")
filename = kwargs.get("filename", "<unknown file>")
+10 -5
View File
@@ -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
@@ -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_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,
@@ -550,10 +551,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),
+1 -1
View File
@@ -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)
+4
View File
@@ -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()
@@ -118,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}")
+21 -5
View File
@@ -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:
+1 -1
View File
@@ -12,7 +12,7 @@ The loader does NOT:
from __future__ import annotations
import importlib
import importlib.util
import re
import sys
import types
+5 -2
View File
@@ -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():
+6
View File
@@ -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:
+1
View File
@@ -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,