Source code for scitex_audio._engines._pyttsx3_engine

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Timestamp: "2025-12-11 (ywatanabe)"
# File: /home/ywatanabe/proj/scitex-code/src/scitex/audio/engines/pyttsx3_engine.py
# ----------------------------------------

"""
System TTS backend using pyttsx3 - Offline, uses system voices.

Requirements:
    - pip install pyttsx3
    - Linux: sudo apt install espeak-ng libespeak1
    - Windows: Uses SAPI5 (built-in)
    - macOS: Uses NSSpeechSynthesizer (built-in)
"""

from __future__ import annotations

from pathlib import Path
from typing import List, Optional

from ._base import BaseTTS

__all__ = ["SystemTTS"]


[docs] class SystemTTS(BaseTTS): """System TTS backend using pyttsx3. Works offline using system's built-in TTS engine. Quality varies by platform and available voices. Platforms: - Linux: espeak/espeak-ng - Windows: SAPI5 - macOS: NSSpeechSynthesizer """ def __init__( self, rate: int = 150, # Words per minute volume: float = 1.0, # 0.0 to 1.0 voice: Optional[str] = None, engine=None, **kwargs, ): super().__init__(**kwargs) self.rate = rate self.volume = volume self.voice = voice # Optional injected pyttsx3 engine (testing). When None, the real # pyttsx3 engine is lazy-initialized on first `engine` access. self._engine = engine @property def name(self) -> str: return "pyttsx3" @property def engine(self): """Lazy-load pyttsx3 engine.""" if self._engine is None: try: import pyttsx3 self._engine = pyttsx3.init() self._engine.setProperty("rate", self.rate) self._engine.setProperty("volume", self.volume) if self.voice: self._set_voice(self.voice) except ImportError: raise ImportError( "pyttsx3 package not installed. " "Install with: pip install pyttsx3\n" "Linux also requires: sudo apt install espeak-ng libespeak1" ) except RuntimeError as e: if "eSpeak" in str(e): raise RuntimeError( "espeak not installed. " "Install with: sudo apt install espeak-ng libespeak1" ) raise return self._engine def _set_voice(self, voice_name: str): """Set voice by name or ID.""" voices = self.engine.getProperty("voices") for v in voices: if voice_name.lower() in v.name.lower() or voice_name == v.id: self.engine.setProperty("voice", v.id) return # If not found, keep default
[docs] def synthesize(self, text: str, output_path: str) -> Path: """Synthesize text using system TTS.""" # Set voice if specified in config voice = self.config.get("voice") if voice: self._set_voice(voice) out_path = Path(output_path) # pyttsx3 can save to file self.engine.save_to_file(text, str(out_path)) self.engine.runAndWait() return out_path
[docs] def speak_direct(self, text: str): """Speak directly without saving to file (faster).""" voice = self.config.get("voice") if voice: self._set_voice(voice) self.engine.say(text) self.engine.runAndWait()
[docs] def get_voices(self) -> List[dict]: """Get available system voices.""" voices = self.engine.getProperty("voices") return [ { "name": v.name, "id": v.id, "type": "system", "languages": getattr(v, "languages", []), } for v in voices ]
# EOF