stephenwahogo commited on
Commit
df062d6
·
verified ·
1 Parent(s): 37800c1

Upload nicto_ai\voice\tts.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. nicto_ai//voice//tts.py +204 -0
nicto_ai//voice//tts.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ NICTO AI - Text-to-Speech
3
+ Convert text to natural-sounding speech.
4
+
5
+ Supports:
6
+ - Local: piper, coqui-tts, espeak
7
+ - Cloud: OpenAI TTS API, Google Text-to-Speech
8
+ """
9
+
10
+ import os
11
+ import logging
12
+ from typing import Optional
13
+ from dataclasses import dataclass
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class SpeechResult:
20
+ """Result from text-to-speech synthesis"""
21
+ audio_data: bytes
22
+ format: str = "wav" # wav, mp3, ogg
23
+ duration_ms: float = 0.0
24
+ engine: str = "unknown"
25
+ sample_rate: int = 22050
26
+
27
+
28
+ class TextToSpeech:
29
+ """
30
+ Text-to-Speech engine for NICTO voice system.
31
+
32
+ Generates natural-sounding speech from text.
33
+
34
+ Usage:
35
+ tts = TextToSpeech()
36
+ result = tts.synthesize("Hello, how can I help you?")
37
+ # result.audio_data contains the audio bytes
38
+ """
39
+
40
+ def __init__(self, engine: str = "auto", voice: str = "default"):
41
+ """
42
+ Args:
43
+ engine: TTS engine ("piper", "coqui", "cloud", "auto")
44
+ voice: Voice name/style
45
+ """
46
+ self.engine = engine
47
+ self.voice = voice
48
+ self._available_engine = self._detect_engine()
49
+ logger.info("TTS initialized with engine: %s", self._available_engine)
50
+
51
+ def synthesize(self, text: str, voice: Optional[str] = None) -> SpeechResult:
52
+ """
53
+ Convert text to speech.
54
+
55
+ Args:
56
+ text: Text to speak
57
+ voice: Override default voice
58
+
59
+ Returns:
60
+ SpeechResult with audio data
61
+ """
62
+ if not text.strip():
63
+ return SpeechResult(audio_data=b"", engine="none")
64
+
65
+ voice_name = voice or self.voice
66
+
67
+ if self._available_engine == "piper":
68
+ return self._synthesize_piper(text, voice_name)
69
+ elif self._available_engine == "coqui":
70
+ return self._synthesize_coqui(text, voice_name)
71
+ elif self._available_engine == "cloud":
72
+ return self._synthesize_cloud(text, voice_name)
73
+ elif self._available_engine == "espeak":
74
+ return self._synthesize_espeak(text)
75
+ else:
76
+ logger.warning("No TTS engine available")
77
+ return SpeechResult(audio_data=b"", engine="none")
78
+
79
+ def synthesize_to_file(self, text: str, output_path: str, voice: Optional[str] = None) -> bool:
80
+ """Synthesize speech and save to file."""
81
+ result = self.synthesize(text, voice)
82
+ if result.audio_data:
83
+ with open(output_path, "wb") as f:
84
+ f.write(result.audio_data)
85
+ return True
86
+ return False
87
+
88
+ def _detect_engine(self) -> str:
89
+ """Detect the best available TTS engine."""
90
+ if self.engine != "auto":
91
+ return self.engine
92
+
93
+ # Try local engines
94
+ import shutil
95
+ if shutil.which("piper"):
96
+ return "piper"
97
+
98
+ try:
99
+ from TTS.api import TTS
100
+ return "coqui"
101
+ except ImportError:
102
+ pass
103
+
104
+ if shutil.which("espeak"):
105
+ return "espeak"
106
+
107
+ if os.environ.get("OPENAI_API_KEY"):
108
+ return "cloud"
109
+
110
+ logger.debug("No TTS engine available. Install piper or coqui-tts.")
111
+ return "none"
112
+
113
+ def _synthesize_piper(self, text: str, voice: str) -> SpeechResult:
114
+ """Synthesize using piper (local, fast)."""
115
+ import subprocess
116
+ import tempfile
117
+ import os
118
+
119
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
120
+ temp_path = f.name
121
+
122
+ try:
123
+ cmd = ["piper", "--model", voice, "--output_file", temp_path]
124
+ proc = subprocess.run(cmd, input=text.encode(), capture_output=True, timeout=30)
125
+
126
+ if proc.returncode == 0 and os.path.exists(temp_path):
127
+ with open(temp_path, "rb") as f:
128
+ audio_data = f.read()
129
+ return SpeechResult(audio_data=audio_data, engine="piper")
130
+ else:
131
+ logger.error("Piper failed: %s", proc.stderr.decode())
132
+ return SpeechResult(audio_data=b"", engine="piper")
133
+ finally:
134
+ if os.path.exists(temp_path):
135
+ os.unlink(temp_path)
136
+
137
+ def _synthesize_coqui(self, text: str, voice: str) -> SpeechResult:
138
+ """Synthesize using coqui-tts (local, high quality)."""
139
+ try:
140
+ from TTS.api import TTS
141
+ import tempfile
142
+ import os
143
+
144
+ tts = TTS(model_name="tts_models/en/ljspeech/tacotron2-DDC")
145
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
146
+ temp_path = f.name
147
+
148
+ try:
149
+ tts.tts_to_file(text=text, file_path=temp_path)
150
+ with open(temp_path, "rb") as f:
151
+ audio_data = f.read()
152
+ return SpeechResult(audio_data=audio_data, engine="coqui")
153
+ finally:
154
+ if os.path.exists(temp_path):
155
+ os.unlink(temp_path)
156
+ except Exception as e:
157
+ logger.error("Coqui TTS failed: %s", e)
158
+ return SpeechResult(audio_data=b"", engine="coqui")
159
+
160
+ def _synthesize_cloud(self, text: str, voice: str) -> SpeechResult:
161
+ """Synthesize using OpenAI TTS API (cloud)."""
162
+ import requests
163
+
164
+ api_key = os.environ.get("OPENAI_API_KEY")
165
+ if not api_key:
166
+ return SpeechResult(audio_data=b"", engine="cloud")
167
+
168
+ response = requests.post(
169
+ "https://api.openai.com/v1/audio/speech",
170
+ headers={"Authorization": f"Bearer {api_key}"},
171
+ json={
172
+ "model": "tts-1",
173
+ "input": text,
174
+ "voice": voice if voice != "default" else "alloy",
175
+ },
176
+ timeout=30,
177
+ )
178
+
179
+ if response.status_code == 200:
180
+ return SpeechResult(
181
+ audio_data=response.content,
182
+ format="mp3",
183
+ engine="cloud",
184
+ )
185
+ else:
186
+ logger.error("Cloud TTS failed: %s", response.text)
187
+ return SpeechResult(audio_data=b"", engine="cloud")
188
+
189
+ def _synthesize_espeak(self, text: str) -> SpeechResult:
190
+ """Synthesize using espeak (basic, always available)."""
191
+ import subprocess
192
+
193
+ try:
194
+ proc = subprocess.run(
195
+ ["espeak", "-w", "/dev/stdout", text],
196
+ capture_output=True,
197
+ timeout=10,
198
+ )
199
+ if proc.returncode == 0:
200
+ return SpeechResult(audio_data=proc.stdout, engine="espeak")
201
+ except Exception as e:
202
+ logger.error("espeak failed: %s", e)
203
+
204
+ return SpeechResult(audio_data=b"", engine="espeak")