← Whispering

fix: resolve espeak-ng vs legacy espeak conflict for Kokoro TTS

7b790abbd66b2e3eb75a0da396cd52bf2fb7d007 · Claude · Feb 10, 2026 21:25

When both espeak (1.48) and espeak-ng (1.52) are installed (common on
Arch/Manjaro), phonemizer may fall back to the old espeak which lacks
the "tie" option (requires >=1.49), causing synthesis to fail.

- Add _ensure_espeak_ng() to KokoroTTSProvider that probes for the
  espeak-ng shared library and sets PHONEMIZER_ESPEAK_LIBRARY before
  kokoro/phonemizer imports
- Improve install.sh pacman handling to warn about the conflict

https://claude.ai/code/session_01965m1v3f1cK6gDV1JHioA8

+46 −2 · 2 files changed

  • scripts/install.sh +9 −2
  • src/tts_provider.py +37 −0
diff --git a/scripts/install.sh b/scripts/install.sh
index e91596c..35b9e1f 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -429,7 +429,10 @@ except ImportError as e:
 install_kokoro() {
     echo -e "${YELLOW}Installing Kokoro TTS...${NC}"
 
-    # Check for espeak-ng system dependency
+    # Check for espeak-ng system dependency.
+    # On Arch/Manjaro the legacy 'espeak' (1.48) package conflicts with
+    # espeak-ng (1.50+).  phonemizer's "tie" option needs >=1.49, so we
+    # must ensure espeak-ng wins.
     if ! command -v espeak-ng &> /dev/null; then
         echo -e "${YELLOW}espeak-ng not found. Attempting to install...${NC}"
         if command -v apt-get &> /dev/null; then
@@ -440,7 +443,11 @@ install_kokoro() {
                 echo -e "${BLUE}  Please install manually: sudo apt-get install espeak-ng${NC}"
             }
         elif command -v pacman &> /dev/null; then
-            sudo pacman -S --noconfirm espeak-ng 2>/dev/null || true
+            # espeak-ng conflicts with legacy espeak; let pacman resolve it
+            sudo pacman -S --noconfirm espeak-ng 2>/dev/null || {
+                echo -e "${YELLOW}⚠ pacman could not install espeak-ng (legacy espeak conflict?)${NC}"
+                echo -e "${BLUE}  Try: sudo pacman -Rdd espeak && sudo pacman -S espeak-ng${NC}"
+            }
         elif command -v dnf &> /dev/null; then
             sudo dnf install -y espeak-ng 2>/dev/null || true
         else
diff --git a/src/tts_provider.py b/src/tts_provider.py
index 757b467..1774f8c 100644
--- a/src/tts_provider.py
+++ b/src/tts_provider.py
@@ -655,11 +655,48 @@ class KokoroTTSProvider(BaseTTSProvider):
                     "p": "p"}.get(voice[0], "a")
         return "a"
 
+    @staticmethod
+    def _ensure_espeak_ng():
+        """Point phonemizer at the espeak-ng shared library.
+
+        When both legacy ``espeak`` (≤1.48) and ``espeak-ng`` (≥1.50) are
+        installed, ``ctypes.util.find_library('espeak-ng')`` may return
+        ``None`` (e.g. on Arch/Manjaro where only the .so symlink exists
+        under a non-standard name), causing phonemizer to fall back to the
+        old ``espeak`` which lacks the *tie* option (requires ≥1.49).
+
+        We probe a few common paths and, if found, set
+        ``PHONEMIZER_ESPEAK_LIBRARY`` so phonemizer picks the right one.
+        """
+        if os.environ.get("PHONEMIZER_ESPEAK_LIBRARY"):
+            return  # user already configured
+
+        # ctypes.util.find_library returns the bare filename (or None)
+        lib = ctypes.util.find_library("espeak-ng")
+        if lib:
+            os.environ["PHONEMIZER_ESPEAK_LIBRARY"] = lib
+            return
+
+        # Manual probe for common distro paths
+        candidates = [
+            "/usr/lib/libespeak-ng.so",
+            "/usr/lib/libespeak-ng.so.1",
+            "/usr/lib64/libespeak-ng.so",
+            "/usr/lib64/libespeak-ng.so.1",
+            "/usr/lib/x86_64-linux-gnu/libespeak-ng.so.1",
+            "/usr/lib/aarch64-linux-gnu/libespeak-ng.so.1",
+        ]
+        for path in candidates:
+            if os.path.isfile(path):
+                os.environ["PHONEMIZER_ESPEAK_LIBRARY"] = path
+                return
+
     def _ensure_initialized(self):
         if self._initialized:
             return
 
         try:
+            self._ensure_espeak_ng()
             from kokoro import KPipeline
 
             tts_timer_reset()