A7med-Ame3's picture
Update tts_engine.py
07350fc verified
"""
tts_engine.py
─────────────
Text-to-Speech engine.
On Hugging Face Spaces (headless server):
- pyttsx3 is skipped (needs audio hardware)
- gTTS saves an MP3 that Gradio can play back via gr.Audio
- Falls back to silent mode gracefully
Locally: pyttsx3 works offline, gTTS needs internet.
"""
import logging
import threading
import os
import io
logger = logging.getLogger(__name__)
class TTSEngine:
def __init__(self, rate: int = 160, volume: float = 1.0):
self._rate = rate
self._volume = volume
self._backend = "silent"
self._init()
def _init(self):
# Try pyttsx3 (local / desktop only)
if os.environ.get("GRADIO_SERVER_NAME") is None:
try:
import pyttsx3
e = pyttsx3.init()
e.setProperty("rate", self._rate)
e.setProperty("volume", self._volume)
self._engine = e
self._backend = "pyttsx3"
logger.info("TTS backend: pyttsx3 (offline)")
return
except Exception as exc:
logger.debug(f"pyttsx3 unavailable: {exc}")
# Try gTTS (online, works on HF Spaces)
try:
import gtts # noqa: F401
self._backend = "gtts"
logger.info("TTS backend: gTTS (online)")
return
except ImportError:
pass
logger.warning("No TTS backend available — speech output disabled.")
# ── Public API ────────────────────────────────────────────────────────────
def speak(self, text: str):
"""Blocking speech."""
if not text:
return
if self._backend == "pyttsx3":
self._engine.say(text)
self._engine.runAndWait()
elif self._backend == "gtts":
self._gtts_speak(text)
else:
logger.info(f"[TTS silent]: {text[:80]}")
def speak_async(self, text: str):
"""Non-blocking TTS in a daemon thread."""
threading.Thread(target=self.speak, args=(text,), daemon=True).start()
def to_audio_bytes(self, text: str) -> bytes | None:
"""
Returns MP3 bytes (for Gradio gr.Audio playback).
Returns None if TTS unavailable.
"""
if self._backend == "gtts":
try:
from gtts import gTTS
buf = io.BytesIO()
gTTS(text=text, lang="en", slow=False).write_to_fp(buf)
return buf.getvalue()
except Exception as exc:
logger.error(f"gTTS error: {exc}")
return None
# ── Helpers ───────────────────────────────────────────────────────────────
def _gtts_speak(self, text: str):
try:
from gtts import gTTS
import tempfile
tts = gTTS(text=text, lang="en", slow=False)
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f:
tts.save(f.name)
tmp = f.name
for player in ("mpg123", "mpg321", "ffplay -nodisp -autoexit"):
if os.system(f"which {player.split()[0]} > /dev/null 2>&1") == 0:
os.system(f"{player} {tmp} > /dev/null 2>&1")
break
os.unlink(tmp)
except Exception as exc:
logger.error(f"gTTS playback error: {exc}")