J Z commited on
Commit
aa635d0
Β·
verified Β·
1 Parent(s): ae7f6ee

Upload 2 files

Browse files
Files changed (2) hide show
  1. groq_tts.py +187 -0
  2. orpheus_tts.py +1 -1
groq_tts.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TTS module - Groq Orpheus API for fast cloud-based speech synthesis.
3
+ Uses the diana voice from canopylabs/orpheus-v1-english.
4
+ No local GPU needed β€” calls Groq's API endpoint.
5
+ """
6
+ import os
7
+ import re
8
+ import time
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ TEMP_DIR = "/tmp/tts_output"
14
+ SAMPLE_RATE = 24000
15
+
16
+ # Groq Orpheus config
17
+ MODEL = "canopylabs/orpheus-v1-english"
18
+ VOICE = os.environ.get("GROQ_TTS_VOICE", "diana")
19
+ RESPONSE_FORMAT = "wav"
20
+
21
+ # Singleton
22
+ _client = None
23
+ _initialized = False
24
+
25
+
26
+ def _ts():
27
+ return time.strftime("%H:%M:%S", time.gmtime()) + f".{int(time.time()*1000)%1000:03d}"
28
+
29
+
30
+ def ensure_temp_dir():
31
+ os.makedirs(TEMP_DIR, exist_ok=True)
32
+ return TEMP_DIR
33
+
34
+
35
+ def initialize():
36
+ """Initialize the Groq client."""
37
+ global _client, _initialized
38
+
39
+ if _initialized:
40
+ return
41
+
42
+ t0 = time.time()
43
+ logger.info(f"[{_ts()}] [TTS] Initializing Groq Orpheus TTS...")
44
+
45
+ from groq import Groq
46
+
47
+ api_key = os.environ.get("GROQ_API_KEY")
48
+ if not api_key:
49
+ logger.error(f"[{_ts()}] [TTS] GROQ_API_KEY not set!")
50
+ return
51
+
52
+ _client = Groq(api_key=api_key)
53
+ _initialized = True
54
+ logger.info(f"[{_ts()}] [TTS] βœ“ Groq Orpheus ready in {time.time()-t0:.2f}s | voice: {VOICE} | model: {MODEL}")
55
+
56
+
57
+ def _clean_text_for_tts(text):
58
+ """Remove tags and asterisk actions."""
59
+ text = re.sub(r'<[^>]+>', '', text)
60
+ text = re.sub(r'\*[^*]+\*', '', text)
61
+ text = re.sub(r'\s+', ' ', text).strip()
62
+ return text
63
+
64
+
65
+ def generate_audio(text: str, output_filename: str = None) -> str:
66
+ """
67
+ Generate speech audio from text using Groq Orpheus API.
68
+ Returns path to wav file, or None on failure.
69
+ """
70
+ if not text or not text.strip():
71
+ return None
72
+
73
+ text = _clean_text_for_tts(text)
74
+ if not text:
75
+ return None
76
+
77
+ if not _initialized or _client is None:
78
+ logger.error(f"[{_ts()}] [TTS] Not initialized!")
79
+ return None
80
+
81
+ temp_dir = ensure_temp_dir()
82
+ if output_filename is None:
83
+ timestamp = int(time.time() * 1000)
84
+ output_filename = f"tts_{timestamp}"
85
+
86
+ if not output_filename.endswith('.wav'):
87
+ output_path = os.path.join(temp_dir, f"{output_filename}.wav")
88
+ else:
89
+ output_path = os.path.join(temp_dir, output_filename)
90
+
91
+ try:
92
+ t0 = time.time()
93
+ logger.info(f"[{_ts()}] [TTS] Generating: {text[:60]}...")
94
+
95
+ # Groq Orpheus has 200 char limit per request β€” split if needed
96
+ chunks = _split_text(text, max_chars=195)
97
+ all_audio = []
98
+
99
+ for i, chunk in enumerate(chunks):
100
+ t1 = time.time()
101
+ response = _client.audio.speech.create(
102
+ model=MODEL,
103
+ voice=VOICE,
104
+ input=chunk,
105
+ response_format=RESPONSE_FORMAT,
106
+ )
107
+
108
+ # Read the audio bytes
109
+ audio_bytes = response.read()
110
+ all_audio.append(audio_bytes)
111
+ logger.info(f"[{_ts()}] [TTS] Chunk {i+1}/{len(chunks)}: {len(chunk)} chars β†’ {len(audio_bytes)/1024:.0f}KB in {time.time()-t1:.2f}s")
112
+
113
+ # If single chunk, write directly; if multiple, concatenate WAV data
114
+ if len(all_audio) == 1:
115
+ with open(output_path, "wb") as f:
116
+ f.write(all_audio[0])
117
+ else:
118
+ _concatenate_wav_files(all_audio, output_path)
119
+
120
+ file_size = os.path.getsize(output_path)
121
+ total = time.time() - t0
122
+
123
+ # Get duration
124
+ import wave
125
+ with wave.open(output_path, "rb") as wf:
126
+ duration = wf.getnframes() / wf.getframerate()
127
+
128
+ logger.info(
129
+ f"[{_ts()}] [TTS] Saved: {output_path} ({file_size/1024:.0f}KB) "
130
+ f"| audio: {duration:.1f}s | total: {total:.2f}s"
131
+ )
132
+ return output_path
133
+
134
+ except Exception as e:
135
+ logger.error(f"[{_ts()}] [TTS] Error: {e}", exc_info=True)
136
+ return None
137
+
138
+
139
+ def _split_text(text: str, max_chars: int = 195) -> list:
140
+ """Split text into chunks under max_chars, breaking at sentence boundaries."""
141
+ if len(text) <= max_chars:
142
+ return [text]
143
+
144
+ chunks = []
145
+ sentences = re.split(r'(?<=[.!?])\s+', text)
146
+ current = ""
147
+
148
+ for sentence in sentences:
149
+ if len(sentence) > max_chars:
150
+ # Single sentence too long β€” split at comma or space
151
+ if current:
152
+ chunks.append(current.strip())
153
+ current = ""
154
+ words = sentence.split()
155
+ for word in words:
156
+ if len(current) + len(word) + 1 > max_chars:
157
+ if current:
158
+ chunks.append(current.strip())
159
+ current = word
160
+ else:
161
+ current = f"{current} {word}" if current else word
162
+ elif len(current) + len(sentence) + 1 > max_chars:
163
+ chunks.append(current.strip())
164
+ current = sentence
165
+ else:
166
+ current = f"{current} {sentence}" if current else sentence
167
+
168
+ if current.strip():
169
+ chunks.append(current.strip())
170
+
171
+ return chunks if chunks else [text]
172
+
173
+
174
+ def _concatenate_wav_files(audio_chunks: list, output_path: str):
175
+ """Concatenate multiple WAV byte chunks into a single WAV file."""
176
+ import wave
177
+ import io
178
+
179
+ # Read first chunk to get params
180
+ with wave.open(io.BytesIO(audio_chunks[0]), "rb") as first:
181
+ params = first.getparams()
182
+
183
+ with wave.open(output_path, "wb") as out:
184
+ out.setparams(params)
185
+ for chunk_bytes in audio_chunks:
186
+ with wave.open(io.BytesIO(chunk_bytes), "rb") as chunk_wav:
187
+ out.writeframes(chunk_wav.readframes(chunk_wav.getnframes()))
orpheus_tts.py CHANGED
@@ -476,4 +476,4 @@ def generate_audio(text: str, output_filename: str = None) -> str:
476
 
477
  except Exception as e:
478
  logger.error(f"[{_ts()}] [TTS] Error: {e}", exc_info=True)
479
- return None
 
476
 
477
  except Exception as e:
478
  logger.error(f"[{_ts()}] [TTS] Error: {e}", exc_info=True)
479
+ return None