A7med-Ame3 commited on
Commit
07350fc
·
verified ·
1 Parent(s): 2408d69

Update tts_engine.py

Browse files
Files changed (1) hide show
  1. tts_engine.py +57 -56
tts_engine.py CHANGED
@@ -1,58 +1,56 @@
1
  """
2
  tts_engine.py
3
  ─────────────
4
- Text-to-Speech engine for reading scene descriptions aloud.
5
 
6
- Priority:
7
- 1. pyttsx3 offline, no network required
8
- 2. gTTS Google TTS (needs internet), saves MP3 and plays
9
- 3. Silent — logs a warning if neither is available
 
 
10
  """
11
 
12
  import logging
13
  import threading
14
  import os
 
15
 
16
  logger = logging.getLogger(__name__)
17
 
18
 
19
  class TTSEngine:
20
- """
21
- Wraps pyttsx3 (preferred) or gTTS (fallback) for async TTS.
22
- """
23
-
24
  def __init__(self, rate: int = 160, volume: float = 1.0):
25
- self._rate = rate
26
- self._volume = volume
27
- self._engine = None
28
- self._lock = threading.Lock()
29
- self._backend: str = "silent"
30
- self._init_engine()
31
 
32
- def _init_engine(self):
33
- # ── Try pyttsx3 ───────────────────────────────────────────────────────
34
- try:
35
- import pyttsx3
36
- engine = pyttsx3.init()
37
- engine.setProperty("rate", self._rate)
38
- engine.setProperty("volume", self._volume)
39
- self._engine = engine
40
- self._backend = "pyttsx3"
41
- logger.info("✅ TTS backend: pyttsx3 (offline)")
42
- return
43
- except Exception as exc:
44
- logger.warning(f"pyttsx3 not available: {exc}")
 
45
 
46
- # ── Try gTTS ──────────────────────────────────────────────────────────
47
  try:
48
  import gtts # noqa: F401
49
  self._backend = "gtts"
50
- logger.info("TTS backend: gTTS (online)")
51
  return
52
  except ImportError:
53
- logger.warning("gTTS not available.")
54
 
55
- logger.warning("⚠️ No TTS backend found — speech output disabled.")
56
 
57
  # ── Public API ────────────────────────────────────────────────────────────
58
 
@@ -61,43 +59,46 @@ class TTSEngine:
61
  if not text:
62
  return
63
  if self._backend == "pyttsx3":
64
- with self._lock:
65
- self._engine.say(text)
66
- self._engine.runAndWait()
67
  elif self._backend == "gtts":
68
- self._speak_gtts(text)
69
  else:
70
- logger.info(f"[TTS silent] {text}")
71
 
72
  def speak_async(self, text: str):
73
- """Non-blocking speech — runs in a daemon thread."""
74
- t = threading.Thread(target=self.speak, args=(text,), daemon=True)
75
- t.start()
76
 
77
- def stop(self):
78
- """Stop current speech (pyttsx3 only)."""
79
- if self._backend == "pyttsx3":
80
- with self._lock:
81
- try:
82
- self._engine.stop()
83
- except Exception:
84
- pass
 
 
 
 
 
 
85
 
86
- # ── Private helpers ───────────────────────────────────────────────────────
87
 
88
- def _speak_gtts(self, text: str):
89
  try:
90
  from gtts import gTTS
91
- import tempfile, subprocess
92
  tts = gTTS(text=text, lang="en", slow=False)
93
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f:
94
  tts.save(f.name)
95
  tmp = f.name
96
- # Play with system player
97
- for player in ("mpg123", "mpg321", "ffplay", "aplay"):
98
- if os.system(f"which {player} > /dev/null 2>&1") == 0:
99
- os.system(f"{player} -q {tmp} > /dev/null 2>&1")
100
  break
101
  os.unlink(tmp)
102
  except Exception as exc:
103
- logger.error(f"gTTS error: {exc}")
 
1
  """
2
  tts_engine.py
3
  ─────────────
4
+ Text-to-Speech engine.
5
 
6
+ On Hugging Face Spaces (headless server):
7
+ - pyttsx3 is skipped (needs audio hardware)
8
+ - gTTS saves an MP3 that Gradio can play back via gr.Audio
9
+ - Falls back to silent mode gracefully
10
+
11
+ Locally: pyttsx3 works offline, gTTS needs internet.
12
  """
13
 
14
  import logging
15
  import threading
16
  import os
17
+ import io
18
 
19
  logger = logging.getLogger(__name__)
20
 
21
 
22
  class TTSEngine:
 
 
 
 
23
  def __init__(self, rate: int = 160, volume: float = 1.0):
24
+ self._rate = rate
25
+ self._volume = volume
26
+ self._backend = "silent"
27
+ self._init()
 
 
28
 
29
+ def _init(self):
30
+ # Try pyttsx3 (local / desktop only)
31
+ if os.environ.get("GRADIO_SERVER_NAME") is None:
32
+ try:
33
+ import pyttsx3
34
+ e = pyttsx3.init()
35
+ e.setProperty("rate", self._rate)
36
+ e.setProperty("volume", self._volume)
37
+ self._engine = e
38
+ self._backend = "pyttsx3"
39
+ logger.info("TTS backend: pyttsx3 (offline)")
40
+ return
41
+ except Exception as exc:
42
+ logger.debug(f"pyttsx3 unavailable: {exc}")
43
 
44
+ # Try gTTS (online, works on HF Spaces)
45
  try:
46
  import gtts # noqa: F401
47
  self._backend = "gtts"
48
+ logger.info("TTS backend: gTTS (online)")
49
  return
50
  except ImportError:
51
+ pass
52
 
53
+ logger.warning("No TTS backend available — speech output disabled.")
54
 
55
  # ── Public API ────────────────────────────────────────────────────────────
56
 
 
59
  if not text:
60
  return
61
  if self._backend == "pyttsx3":
62
+ self._engine.say(text)
63
+ self._engine.runAndWait()
 
64
  elif self._backend == "gtts":
65
+ self._gtts_speak(text)
66
  else:
67
+ logger.info(f"[TTS silent]: {text[:80]}")
68
 
69
  def speak_async(self, text: str):
70
+ """Non-blocking TTS in a daemon thread."""
71
+ threading.Thread(target=self.speak, args=(text,), daemon=True).start()
 
72
 
73
+ def to_audio_bytes(self, text: str) -> bytes | None:
74
+ """
75
+ Returns MP3 bytes (for Gradio gr.Audio playback).
76
+ Returns None if TTS unavailable.
77
+ """
78
+ if self._backend == "gtts":
79
+ try:
80
+ from gtts import gTTS
81
+ buf = io.BytesIO()
82
+ gTTS(text=text, lang="en", slow=False).write_to_fp(buf)
83
+ return buf.getvalue()
84
+ except Exception as exc:
85
+ logger.error(f"gTTS error: {exc}")
86
+ return None
87
 
88
+ # ── Helpers ───────────────────────────────────────────────────────────────
89
 
90
+ def _gtts_speak(self, text: str):
91
  try:
92
  from gtts import gTTS
93
+ import tempfile
94
  tts = gTTS(text=text, lang="en", slow=False)
95
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as f:
96
  tts.save(f.name)
97
  tmp = f.name
98
+ for player in ("mpg123", "mpg321", "ffplay -nodisp -autoexit"):
99
+ if os.system(f"which {player.split()[0]} > /dev/null 2>&1") == 0:
100
+ os.system(f"{player} {tmp} > /dev/null 2>&1")
 
101
  break
102
  os.unlink(tmp)
103
  except Exception as exc:
104
+ logger.error(f"gTTS playback error: {exc}")