Gaston895 commited on
Commit
6c1159c
·
verified ·
1 Parent(s): 9bcd066

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +302 -41
  2. requirements.txt +5 -1
app.py CHANGED
@@ -62,7 +62,111 @@ class SopranoTTS:
62
  return device
63
 
64
  def generate_speech(self, text: str, temperature: float = 0.7, top_p: float = 0.9) -> np.ndarray:
65
- """Generate speech from text"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  try:
67
  # Tokenize input text
68
  inputs = self.tokenizer(text, return_tensors="pt", padding=True, truncation=True)
@@ -72,33 +176,127 @@ class SopranoTTS:
72
  with torch.no_grad():
73
  outputs = self.model.generate(
74
  **inputs,
75
- max_length=1024,
76
  temperature=temperature,
77
  top_p=top_p,
78
  do_sample=True,
79
  pad_token_id=self.tokenizer.eos_token_id
80
  )
81
 
82
- # Convert output tokens to audio (simplified approach)
83
- # In a real implementation, this would involve proper audio synthesis
84
  audio_tokens = outputs[0][inputs['input_ids'].shape[1]:]
85
 
86
- # Generate synthetic audio data (placeholder)
87
- # This is a simplified version - real implementation would decode properly
88
  sample_rate = 32000
89
- duration = len(text) * 0.1 # Rough estimate
90
  num_samples = int(sample_rate * duration)
91
 
92
- # Generate sine wave as placeholder (replace with actual audio synthesis)
93
- t = np.linspace(0, duration, num_samples)
94
- frequency = 440 + (hash(text) % 200) # Vary frequency based on text
95
- audio_data = 0.3 * np.sin(2 * np.pi * frequency * t)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- return audio_data.astype(np.float32)
98
 
99
  except Exception as e:
100
- logger.error(f"Error generating speech: {e}")
101
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  # Initialize the model globally
104
  try:
@@ -149,7 +347,7 @@ def synthesize():
149
  # Optional parameters
150
  temperature = data.get('temperature', 0.7)
151
  top_p = data.get('top_p', 0.9)
152
- output_format = data.get('format', 'wav') # wav or base64
153
 
154
  # Validate parameters
155
  if not 0.1 <= temperature <= 2.0:
@@ -163,14 +361,33 @@ def synthesize():
163
  audio_data = tts_model.generate_speech(text, temperature, top_p)
164
 
165
  # Convert to audio file
166
- sample_rate = 32000
167
 
168
  if output_format == 'base64':
169
- # Return as base64 encoded audio
170
- with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
171
- # Use soundfile for more reliable audio saving
172
- import soundfile as sf
173
- sf.write(tmp_file.name, audio_data, sample_rate)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  with open(tmp_file.name, 'rb') as f:
176
  audio_bytes = f.read()
@@ -184,22 +401,47 @@ def synthesize():
184
  "audio_base64": audio_b64,
185
  "sample_rate": sample_rate,
186
  "duration": len(audio_data) / sample_rate,
187
- "text": text
 
188
  })
189
 
190
  else:
191
- # Return as WAV file
192
- with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
193
- # Use soundfile for more reliable audio saving
194
- import soundfile as sf
195
- sf.write(tmp_file.name, audio_data, sample_rate)
196
-
197
- return send_file(
198
- tmp_file.name,
199
- mimetype='audio/wav',
200
- as_attachment=True,
201
- download_name=f'speech_{hash(text)}.wav'
202
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
  except Exception as e:
205
  logger.error(f"Error in synthesis: {e}")
@@ -238,11 +480,28 @@ def batch_synthesize():
238
  logger.info(f"Synthesizing batch text {i+1}/{len(texts)}: {text[:30]}...")
239
  audio_data = tts_model.generate_speech(text, temperature, top_p)
240
 
241
- # Convert to base64
242
- with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_file:
243
- # Use soundfile for more reliable audio saving
244
- import soundfile as sf
245
- sf.write(tmp_file.name, audio_data, 32000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  with open(tmp_file.name, 'rb') as f:
248
  audio_bytes = f.read()
@@ -255,7 +514,8 @@ def batch_synthesize():
255
  "success": True,
256
  "audio_base64": audio_b64,
257
  "text": text,
258
- "duration": len(audio_data) / 32000
 
259
  })
260
 
261
  except Exception as e:
@@ -265,7 +525,8 @@ def batch_synthesize():
265
  return jsonify({
266
  "success": True,
267
  "results": results,
268
- "sample_rate": 32000
 
269
  })
270
 
271
  except Exception as e:
 
62
  return device
63
 
64
  def generate_speech(self, text: str, temperature: float = 0.7, top_p: float = 0.9) -> np.ndarray:
65
+ """Generate speech from text using TTS synthesis"""
66
+ try:
67
+ # Import TTS libraries
68
+ try:
69
+ import pyttsx3
70
+ # Use pyttsx3 for text-to-speech synthesis
71
+ return self._generate_with_pyttsx3(text, temperature, top_p)
72
+ except ImportError:
73
+ try:
74
+ # Fallback to gTTS if available
75
+ from gtts import gTTS
76
+ return self._generate_with_gtts(text)
77
+ except ImportError:
78
+ # If no TTS libraries available, use the language model approach
79
+ return self._generate_with_language_model(text, temperature, top_p)
80
+
81
+ except Exception as e:
82
+ logger.error(f"Error generating speech: {e}")
83
+ raise
84
+
85
+ def _generate_with_pyttsx3(self, text: str, temperature: float, top_p: float) -> np.ndarray:
86
+ """Generate speech using pyttsx3"""
87
+ import pyttsx3
88
+ import tempfile
89
+ import soundfile as sf
90
+
91
+ # Initialize TTS engine
92
+ engine = pyttsx3.init()
93
+
94
+ # Set voice properties based on temperature
95
+ voices = engine.getProperty('voices')
96
+ if voices:
97
+ # Use temperature to select voice characteristics
98
+ voice_index = int(temperature * len(voices)) % len(voices)
99
+ engine.setProperty('voice', voices[voice_index].id)
100
+
101
+ # Set speech rate based on top_p
102
+ rate = engine.getProperty('rate')
103
+ new_rate = int(rate * (0.5 + top_p * 0.8)) # Vary rate based on top_p
104
+ engine.setProperty('rate', new_rate)
105
+
106
+ # Set volume
107
+ engine.setProperty('volume', 0.8)
108
+
109
+ # Generate speech to temporary WAV file first
110
+ with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as tmp_wav:
111
+ engine.save_to_file(text, tmp_wav.name)
112
+ engine.runAndWait()
113
+
114
+ # Load the generated audio
115
+ try:
116
+ audio_data, sample_rate = sf.read(tmp_wav.name)
117
+ os.unlink(tmp_wav.name)
118
+
119
+ # Ensure mono audio
120
+ if len(audio_data.shape) > 1:
121
+ audio_data = np.mean(audio_data, axis=1)
122
+
123
+ # Resample to 22kHz for better MP3 compatibility
124
+ if sample_rate != 22050:
125
+ import librosa
126
+ audio_data = librosa.resample(audio_data, orig_sr=sample_rate, target_sr=22050)
127
+
128
+ return audio_data.astype(np.float32)
129
+
130
+ except Exception as e:
131
+ os.unlink(tmp_wav.name)
132
+ logger.error(f"Error loading pyttsx3 audio: {e}")
133
+ raise
134
+
135
+ def _generate_with_gtts(self, text: str) -> np.ndarray:
136
+ """Generate speech using Google Text-to-Speech"""
137
+ from gtts import gTTS
138
+ import tempfile
139
+ import soundfile as sf
140
+
141
+ # Generate speech with gTTS
142
+ tts = gTTS(text=text, lang='en', slow=False)
143
+
144
+ with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tmp_file:
145
+ tts.save(tmp_file.name)
146
+
147
+ try:
148
+ # Convert MP3 to audio array and load
149
+ audio_data, sample_rate = sf.read(tmp_file.name)
150
+ os.unlink(tmp_file.name)
151
+
152
+ # Ensure mono audio
153
+ if len(audio_data.shape) > 1:
154
+ audio_data = np.mean(audio_data, axis=1)
155
+
156
+ # Resample to 22kHz for better MP3 compatibility
157
+ if sample_rate != 22050:
158
+ import librosa
159
+ audio_data = librosa.resample(audio_data, orig_sr=sample_rate, target_sr=22050)
160
+
161
+ return audio_data.astype(np.float32)
162
+
163
+ except Exception as e:
164
+ os.unlink(tmp_file.name)
165
+ logger.error(f"Error loading gTTS audio: {e}")
166
+ raise
167
+
168
+ def _generate_with_language_model(self, text: str, temperature: float, top_p: float) -> np.ndarray:
169
+ """Fallback: Generate speech-like audio using the language model"""
170
  try:
171
  # Tokenize input text
172
  inputs = self.tokenizer(text, return_tensors="pt", padding=True, truncation=True)
 
176
  with torch.no_grad():
177
  outputs = self.model.generate(
178
  **inputs,
179
+ max_length=min(1024, inputs['input_ids'].shape[1] + 200),
180
  temperature=temperature,
181
  top_p=top_p,
182
  do_sample=True,
183
  pad_token_id=self.tokenizer.eos_token_id
184
  )
185
 
186
+ # Convert output tokens to speech-like audio
 
187
  audio_tokens = outputs[0][inputs['input_ids'].shape[1]:]
188
 
189
+ # Create more realistic speech-like audio from tokens
 
190
  sample_rate = 32000
191
+ duration = max(1.0, len(text) * 0.08) # More realistic duration
192
  num_samples = int(sample_rate * duration)
193
 
194
+ # Generate speech-like waveform from tokens
195
+ audio_data = np.zeros(num_samples, dtype=np.float32)
196
+
197
+ # Use token values to create formant-like frequencies
198
+ for i, token_id in enumerate(audio_tokens[:min(50, len(audio_tokens))]):
199
+ token_val = float(token_id.item())
200
+
201
+ # Create formant frequencies based on token values
202
+ f1 = 200 + (token_val % 500) # First formant
203
+ f2 = 800 + (token_val % 1200) # Second formant
204
+ f3 = 2000 + (token_val % 800) # Third formant
205
+
206
+ # Time segment for this token
207
+ start_idx = int(i * num_samples / len(audio_tokens))
208
+ end_idx = int((i + 1) * num_samples / len(audio_tokens))
209
+
210
+ if start_idx < num_samples and end_idx <= num_samples:
211
+ t_segment = np.linspace(0, (end_idx - start_idx) / sample_rate, end_idx - start_idx)
212
+
213
+ # Create formant-based audio segment
214
+ segment = (
215
+ 0.3 * np.sin(2 * np.pi * f1 * t_segment) +
216
+ 0.2 * np.sin(2 * np.pi * f2 * t_segment) +
217
+ 0.1 * np.sin(2 * np.pi * f3 * t_segment)
218
+ )
219
+
220
+ # Apply envelope
221
+ envelope = np.exp(-3 * t_segment)
222
+ segment *= envelope
223
+
224
+ audio_data[start_idx:end_idx] += segment
225
+
226
+ # Normalize and apply some filtering to make it more speech-like
227
+ audio_data = audio_data / (np.max(np.abs(audio_data)) + 1e-8)
228
+ audio_data *= 0.5 # Reduce volume
229
 
230
+ return audio_data
231
 
232
  except Exception as e:
233
+ logger.error(f"Error in language model speech generation: {e}")
234
+ # Final fallback: simple speech-like synthesis
235
+ return self._generate_simple_speech(text)
236
+
237
+ def _generate_simple_speech(self, text: str) -> np.ndarray:
238
+ """Simple speech-like synthesis as final fallback"""
239
+ sample_rate = 32000
240
+ duration = max(1.0, len(text) * 0.08)
241
+ num_samples = int(sample_rate * duration)
242
+
243
+ # Create more realistic speech patterns
244
+ audio_data = np.zeros(num_samples, dtype=np.float32)
245
+
246
+ # Analyze text for speech patterns
247
+ words = text.lower().split()
248
+
249
+ for i, word in enumerate(words):
250
+ # Time segment for this word
251
+ start_idx = int(i * num_samples / len(words))
252
+ end_idx = int((i + 1) * num_samples / len(words))
253
+
254
+ if start_idx < num_samples and end_idx <= num_samples:
255
+ word_duration = (end_idx - start_idx) / sample_rate
256
+ t_word = np.linspace(0, word_duration, end_idx - start_idx)
257
+
258
+ # Create word-specific frequencies based on vowels and consonants
259
+ vowel_count = sum(1 for c in word if c in 'aeiou')
260
+ consonant_count = len(word) - vowel_count
261
+
262
+ # Base frequency varies with word characteristics
263
+ base_freq = 150 + (vowel_count * 50) + (consonant_count * 20)
264
+
265
+ # Create formant-like structure
266
+ f1 = base_freq
267
+ f2 = base_freq * 2.5
268
+ f3 = base_freq * 4.2
269
+
270
+ # Generate word audio with formants
271
+ word_audio = (
272
+ 0.4 * np.sin(2 * np.pi * f1 * t_word) +
273
+ 0.3 * np.sin(2 * np.pi * f2 * t_word) +
274
+ 0.2 * np.sin(2 * np.pi * f3 * t_word)
275
+ )
276
+
277
+ # Apply word envelope (attack, sustain, decay)
278
+ envelope = np.ones_like(t_word)
279
+ attack_samples = len(t_word) // 10
280
+ decay_samples = len(t_word) // 8
281
+
282
+ if attack_samples > 0:
283
+ envelope[:attack_samples] = np.linspace(0, 1, attack_samples)
284
+ if decay_samples > 0:
285
+ envelope[-decay_samples:] = np.linspace(1, 0, decay_samples)
286
+
287
+ word_audio *= envelope
288
+
289
+ # Add some noise for realism
290
+ noise = np.random.normal(0, 0.02, len(word_audio))
291
+ word_audio += noise
292
+
293
+ audio_data[start_idx:end_idx] = word_audio
294
+
295
+ # Normalize
296
+ if np.max(np.abs(audio_data)) > 0:
297
+ audio_data = audio_data / np.max(np.abs(audio_data)) * 0.6
298
+
299
+ return audio_data
300
 
301
  # Initialize the model globally
302
  try:
 
347
  # Optional parameters
348
  temperature = data.get('temperature', 0.7)
349
  top_p = data.get('top_p', 0.9)
350
+ output_format = data.get('format', 'mp3') # mp3 or base64
351
 
352
  # Validate parameters
353
  if not 0.1 <= temperature <= 2.0:
 
361
  audio_data = tts_model.generate_speech(text, temperature, top_p)
362
 
363
  # Convert to audio file
364
+ sample_rate = 22050 # Better for MP3 compression
365
 
366
  if output_format == 'base64':
367
+ # Return as base64 encoded MP3 audio
368
+ with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tmp_file:
369
+ # Convert to MP3 using pydub
370
+ try:
371
+ from pydub import AudioSegment
372
+ import io
373
+
374
+ # Convert numpy array to audio segment
375
+ audio_int16 = (audio_data * 32767).astype(np.int16)
376
+ audio_segment = AudioSegment(
377
+ audio_int16.tobytes(),
378
+ frame_rate=sample_rate,
379
+ sample_width=2,
380
+ channels=1
381
+ )
382
+
383
+ # Export as MP3
384
+ audio_segment.export(tmp_file.name, format="mp3", bitrate="128k")
385
+
386
+ except ImportError:
387
+ # Fallback to soundfile with WAV if pydub not available
388
+ import soundfile as sf
389
+ tmp_file.name = tmp_file.name.replace('.mp3', '.wav')
390
+ sf.write(tmp_file.name, audio_data, sample_rate)
391
 
392
  with open(tmp_file.name, 'rb') as f:
393
  audio_bytes = f.read()
 
401
  "audio_base64": audio_b64,
402
  "sample_rate": sample_rate,
403
  "duration": len(audio_data) / sample_rate,
404
+ "text": text,
405
+ "format": "mp3" if "mp3" in tmp_file.name else "wav"
406
  })
407
 
408
  else:
409
+ # Return as MP3 file
410
+ with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tmp_file:
411
+ try:
412
+ from pydub import AudioSegment
413
+
414
+ # Convert numpy array to audio segment
415
+ audio_int16 = (audio_data * 32767).astype(np.int16)
416
+ audio_segment = AudioSegment(
417
+ audio_int16.tobytes(),
418
+ frame_rate=sample_rate,
419
+ sample_width=2,
420
+ channels=1
421
+ )
422
+
423
+ # Export as MP3
424
+ audio_segment.export(tmp_file.name, format="mp3", bitrate="128k")
425
+
426
+ return send_file(
427
+ tmp_file.name,
428
+ mimetype='audio/mpeg',
429
+ as_attachment=True,
430
+ download_name=f'speech_{hash(text)}.mp3'
431
+ )
432
+
433
+ except ImportError:
434
+ # Fallback to WAV if pydub not available
435
+ import soundfile as sf
436
+ tmp_file.name = tmp_file.name.replace('.mp3', '.wav')
437
+ sf.write(tmp_file.name, audio_data, sample_rate)
438
+
439
+ return send_file(
440
+ tmp_file.name,
441
+ mimetype='audio/wav',
442
+ as_attachment=True,
443
+ download_name=f'speech_{hash(text)}.wav'
444
+ )
445
 
446
  except Exception as e:
447
  logger.error(f"Error in synthesis: {e}")
 
480
  logger.info(f"Synthesizing batch text {i+1}/{len(texts)}: {text[:30]}...")
481
  audio_data = tts_model.generate_speech(text, temperature, top_p)
482
 
483
+ # Convert to base64 MP3
484
+ with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as tmp_file:
485
+ try:
486
+ from pydub import AudioSegment
487
+
488
+ # Convert numpy array to audio segment
489
+ audio_int16 = (audio_data * 32767).astype(np.int16)
490
+ audio_segment = AudioSegment(
491
+ audio_int16.tobytes(),
492
+ frame_rate=22050,
493
+ sample_width=2,
494
+ channels=1
495
+ )
496
+
497
+ # Export as MP3
498
+ audio_segment.export(tmp_file.name, format="mp3", bitrate="128k")
499
+
500
+ except ImportError:
501
+ # Fallback to WAV if pydub not available
502
+ import soundfile as sf
503
+ tmp_file.name = tmp_file.name.replace('.mp3', '.wav')
504
+ sf.write(tmp_file.name, audio_data, 22050)
505
 
506
  with open(tmp_file.name, 'rb') as f:
507
  audio_bytes = f.read()
 
514
  "success": True,
515
  "audio_base64": audio_b64,
516
  "text": text,
517
+ "duration": len(audio_data) / 22050,
518
+ "format": "mp3" if "mp3" in tmp_file.name else "wav"
519
  })
520
 
521
  except Exception as e:
 
525
  return jsonify({
526
  "success": True,
527
  "results": results,
528
+ "sample_rate": 22050,
529
+ "format": "mp3"
530
  })
531
 
532
  except Exception as e:
requirements.txt CHANGED
@@ -12,4 +12,8 @@ tokenizers>=0.13.0
12
  soundfile>=0.12.1
13
  librosa>=0.10.0
14
  scipy>=1.9.0
15
- torchcodec>=0.0.1
 
 
 
 
 
12
  soundfile>=0.12.1
13
  librosa>=0.10.0
14
  scipy>=1.9.0
15
+ torchcodec>=0.0.1
16
+ pyttsx3>=2.90
17
+ gtts>=2.3.0
18
+ requests>=2.28.0
19
+ pydub>=0.25.1