yukee1992 commited on
Commit
fbab4cf
·
verified ·
1 Parent(s): 7ff4a36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +17 -340
app.py CHANGED
@@ -19,7 +19,12 @@ os.makedirs("/tmp/voices", exist_ok=True)
19
  os.makedirs("/tmp/output", exist_ok=True)
20
 
21
  # Initialize FastAPI app
22
- app = FastAPI(title="TTS API", description="API for text-to-speech with Coqui TTS and voice cloning")
 
 
 
 
 
23
 
24
  # Add CORS middleware
25
  app.add_middleware(
@@ -30,10 +35,9 @@ app.add_middleware(
30
  allow_headers=["*"],
31
  )
32
 
33
- # Configuration - FIXED OCI URL HANDLING
34
  OCI_UPLOAD_API_URL = os.getenv("OCI_UPLOAD_API_URL", "").strip()
35
  if OCI_UPLOAD_API_URL:
36
- # Remove trailing slash if present
37
  OCI_UPLOAD_API_URL = OCI_UPLOAD_API_URL.rstrip('/')
38
 
39
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
@@ -45,10 +49,7 @@ print(f"🔧 OCI Upload URL: {OCI_UPLOAD_API_URL or 'Not configured - uploads wi
45
  tts = None
46
  model_loaded = False
47
  current_model = ""
48
- voice_cloning_supported = False
49
  model_loading = False
50
- model_load_attempts = 0
51
- current_voice_style = "default_female"
52
  app_startup_time = datetime.now()
53
 
54
  # Pydantic models
@@ -66,14 +67,6 @@ class BatchTTSRequest(BaseModel):
66
  language: Optional[str] = "en"
67
  voice_style: Optional[str] = "default_female"
68
 
69
- class VoiceCloneRequest(BaseModel):
70
- project_id: str
71
- voice_name: str
72
- description: Optional[str] = ""
73
-
74
- class ChangeVoiceRequest(BaseModel):
75
- voice_style: str
76
-
77
  # Helper functions
78
  def clean_text(text):
79
  """Clean text for TTS generation"""
@@ -106,12 +99,10 @@ def clean_text(text):
106
  def upload_to_oci(file_path: str, filename: str, project_id: str, file_type="voiceover"):
107
  """Upload file to OCI using your existing API with subfolder support"""
108
  try:
109
- # Check if OCI URL is properly configured
110
  if not OCI_UPLOAD_API_URL:
111
  print("⚠️ OCI upload skipped - OCI_UPLOAD_API_URL not configured")
112
  return {"status": "skipped", "message": "OCI upload disabled"}, None
113
 
114
- # Validate URL format
115
  if not OCI_UPLOAD_API_URL.startswith(('http://', 'https://')):
116
  return None, f"Invalid OCI URL format: {OCI_UPLOAD_API_URL}"
117
 
@@ -157,24 +148,12 @@ def upload_to_oci(file_path: str, filename: str, project_id: str, file_type="voi
157
  print(f"❌ {error_msg}")
158
  return None, error_msg
159
 
160
- except requests.exceptions.Timeout:
161
- error_msg = "OCI upload timeout - server took too long to respond"
162
- print(f"❌ {error_msg}")
163
- return None, error_msg
164
- except requests.exceptions.ConnectionError:
165
- error_msg = f"Cannot connect to OCI API at {OCI_UPLOAD_API_URL} - check if the server is running and accessible"
166
- print(f"❌ {error_msg}")
167
- return None, error_msg
168
- except requests.exceptions.RequestException as e:
169
- error_msg = f"Network error during upload: {str(e)}"
170
- print(f"❌ {error_msg}")
171
- return None, error_msg
172
  except Exception as e:
173
- error_msg = f"Unexpected upload error: {str(e)}"
174
  print(f"❌ {error_msg}")
175
  return None, error_msg
176
 
177
- def upload_to_oci_with_retry(file_path: str, filename: str, project_id: str, file_type="voiceover", max_retries=3):
178
  """Upload file to OCI with retry logic"""
179
  for attempt in range(max_retries):
180
  try:
@@ -203,42 +182,6 @@ def upload_to_oci_with_retry(file_path: str, filename: str, project_id: str, fil
203
 
204
  return None, "Upload failed: unexpected error"
205
 
206
- def get_voice_path(voice_name: str):
207
- """Get path to voice file"""
208
- if voice_name == "default":
209
- return None
210
-
211
- voice_path = Path(f"/tmp/voices/{voice_name}")
212
- if voice_path.is_dir():
213
- samples = list(voice_path.glob("sample_*.wav"))
214
- return str(samples[0]) if samples else None
215
- else:
216
- voice_file = Path(f"/tmp/voices/{voice_name}.wav")
217
- return str(voice_file) if voice_file.exists() else None
218
-
219
- def clone_voice(voice_name: str, audio_files: List[str], description: str = ""):
220
- """Clone a voice from audio samples"""
221
- try:
222
- print(f"🎙️ Cloning voice: {voice_name}")
223
-
224
- voice_dir = f"/tmp/voices/{voice_name}"
225
- os.makedirs(voice_dir, exist_ok=True)
226
-
227
- for i, audio_file in enumerate(audio_files):
228
- dest_path = f"{voice_dir}/sample_{i+1}.wav"
229
- shutil.copy2(audio_file, dest_path)
230
- print(f" Copied sample {i+1} to: {dest_path}")
231
-
232
- print(f"✅ Voice cloning setup completed for {voice_name}")
233
- return True, f"Voice {voice_name} is ready for use"
234
-
235
- except Exception as e:
236
- return False, f"Voice cloning failed: {str(e)}"
237
-
238
- def supports_voice_cloning():
239
- """Check if the current model supports voice cloning"""
240
- return "xtts" in current_model.lower()
241
-
242
  def save_wav(audio, file_path):
243
  """Save audio to WAV file manually"""
244
  try:
@@ -273,62 +216,37 @@ def save_wav(audio, file_path):
273
 
274
  def load_tts_model(voice_style="default_female"):
275
  """Load TTS model with different voice options - LAZY LOADING"""
276
- global tts, model_loaded, current_model, voice_cloning_supported, model_loading, model_load_attempts, current_voice_style
277
 
278
  if model_loading:
279
  print("⏳ Model is already being loaded...")
280
  return False
281
 
282
- if model_loaded and current_voice_style == voice_style:
283
- print("✅ Model already loaded with requested voice style")
284
  return True
285
 
286
  model_loading = True
287
- model_load_attempts += 1
288
 
289
  try:
290
  from TTS.api import TTS
291
 
292
- # Use smaller, faster models for initial load
293
  model_options = {
294
  "default_female": {
295
  "name": "tts_models/en/ljspeech/tacotron2-DDC",
296
  "description": "Tacotron2 - Default female (fast)",
297
- "speaker": None
298
  },
299
  "clear_male": {
300
  "name": "tts_models/en/ek1/tacotron2",
301
  "description": "Tacotron2 - Clear male voice",
302
- "speaker": None
303
- },
304
- # Fallbacks for other voice styles
305
- "male_deep": {
306
- "name": "tts_models/en/ljspeech/tacotron2-DDC",
307
- "description": "Tacotron2 - Default female (fallback)",
308
- "speaker": None
309
- },
310
- "male_medium": {
311
- "name": "tts_models/en/ljspeech/tacotron2-DDC",
312
- "description": "Tacotron2 - Default female (fallback)",
313
- "speaker": None
314
- },
315
- "female_1": {
316
- "name": "tts_models/en/ljspeech/tacotron2-DDC",
317
- "description": "Tacotron2 - Default female (fallback)",
318
- "speaker": None
319
- },
320
- "female_2": {
321
- "name": "tts_models/en/ljspeech/tacotron2-DDC",
322
- "description": "Tacotron2 - Default female (fallback)",
323
- "speaker": None
324
  }
325
  }
326
 
327
  selected_model = model_options.get(voice_style, model_options["default_female"])
328
- current_voice_style = voice_style
329
 
330
  print(f"🚀 Loading {selected_model['description']}...")
331
- print(f"📥 This may take a few minutes on first load...")
332
 
333
  # Load the selected model
334
  tts = TTS(selected_model["name"]).to(DEVICE)
@@ -348,8 +266,6 @@ def load_tts_model(voice_style="default_female"):
348
 
349
  model_loaded = True
350
  current_model = selected_model["name"]
351
- voice_cloning_supported = False
352
- model_load_attempts = 0
353
  return True
354
 
355
  except Exception as e:
@@ -360,18 +276,6 @@ def load_tts_model(voice_style="default_female"):
360
  model_loading = False
361
 
362
  # Health check endpoints - CRITICAL FOR DEPLOYMENT
363
- @app.get("/")
364
- async def root():
365
- """Root endpoint - always responds quickly"""
366
- return {
367
- "status": "running",
368
- "service": "TTS API",
369
- "startup_time": app_startup_time.isoformat(),
370
- "model_loaded": model_loaded,
371
- "device": DEVICE,
372
- "oci_configured": bool(OCI_UPLOAD_API_URL)
373
- }
374
-
375
  @app.get("/health")
376
  async def health_check():
377
  """Health check endpoint - must respond quickly"""
@@ -405,7 +309,6 @@ async def check_oci_health():
405
  }
406
 
407
  try:
408
- # Test connection to OCI service with short timeout
409
  test_url = f"{OCI_UPLOAD_API_URL}/api/health"
410
  response = requests.get(test_url, timeout=5)
411
 
@@ -434,7 +337,7 @@ async def generate_tts(request: TTSRequest):
434
  """Generate TTS for a single text with lazy model loading"""
435
  try:
436
  # Lazy load model on first request
437
- if not model_loaded or current_voice_style != request.voice_style:
438
  print("🔄 Lazy loading TTS model...")
439
  if not load_tts_model(request.voice_style):
440
  return {
@@ -448,14 +351,6 @@ async def generate_tts(request: TTSRequest):
448
  print(f" Text length: {len(request.text)} characters")
449
  print(f" Voice style: {request.voice_style}")
450
 
451
- # Check if voice cloning is requested but not supported
452
- if request.voice_name != "default" and not supports_voice_cloning():
453
- return {
454
- "status": "error",
455
- "message": "Voice cloning is not supported with the current model. Only the default voice is available.",
456
- "model": current_model
457
- }
458
-
459
  # Generate unique filename
460
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
461
  filename = f"voiceover_{timestamp}.wav"
@@ -464,16 +359,6 @@ async def generate_tts(request: TTSRequest):
464
  # Ensure output directory exists
465
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
466
 
467
- # Get voice path if custom voice is requested
468
- speaker_wav = None
469
- if request.voice_name != "default":
470
- speaker_wav = get_voice_path(request.voice_name)
471
- if not speaker_wav:
472
- return {
473
- "status": "error",
474
- "message": f"Voice '{request.voice_name}' not found"
475
- }
476
-
477
  print(f"🔊 Generating TTS to: {output_path}")
478
 
479
  # Clean the text before generation
@@ -553,218 +438,12 @@ async def generate_tts(request: TTSRequest):
553
  print(f"❌ TTS generation error: {str(e)}")
554
  raise HTTPException(status_code=500, detail=f"TTS generation failed: {str(e)}")
555
 
556
- @app.post("/api/batch-tts")
557
- async def batch_generate_tts(request: BatchTTSRequest):
558
- """Generate TTS for multiple texts with sequential naming"""
559
- try:
560
- # Lazy load model on first request
561
- if not model_loaded or current_voice_style != request.voice_style:
562
- print("🔄 Lazy loading TTS model for batch processing...")
563
- if not load_tts_model(request.voice_style):
564
- raise HTTPException(status_code=500, detail="TTS model failed to load")
565
-
566
- print(f"📥 Batch TTS request for project: {request.project_id}")
567
- print(f" Number of texts: {len(request.texts)}")
568
- print(f" Voice style: {request.voice_style}")
569
-
570
- # Check if voice cloning is requested but not supported
571
- if request.voice_name != "default" and not supports_voice_cloning():
572
- raise HTTPException(
573
- status_code=400,
574
- detail="Voice cloning is not supported with the current model. Only the default voice is available."
575
- )
576
-
577
- # Get voice path if custom voice is requested
578
- speaker_wav = None
579
- if request.voice_name != "default":
580
- speaker_wav = get_voice_path(request.voice_name)
581
- if not speaker_wav:
582
- raise HTTPException(status_code=400, detail=f"Voice '{request.voice_name}' not found")
583
-
584
- results = []
585
-
586
- for i, text in enumerate(request.texts):
587
- print(f" Processing text {i+1}/{len(request.texts)}")
588
-
589
- # Generate sequential filename
590
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
591
- filename = f"voiceover_{timestamp}_{i+1:03d}.wav"
592
- output_path = f"/tmp/output/{filename}"
593
-
594
- # Clean text
595
- cleaned_text = clean_text(text)
596
-
597
- # Generate TTS
598
- try:
599
- tts.tts_to_file(text=cleaned_text, file_path=output_path)
600
-
601
- # Verify file was created
602
- if not os.path.exists(output_path):
603
- raise Exception(f"Failed to create output file: {output_path}")
604
-
605
- file_size = os.path.getsize(output_path)
606
-
607
- # Upload to OCI
608
- upload_result, error = upload_to_oci_with_retry(
609
- output_path, filename, request.project_id, "voiceover"
610
- )
611
-
612
- if error:
613
- results.append({
614
- "status": "success_local",
615
- "filename": filename,
616
- "local_file": output_path,
617
- "file_size": file_size,
618
- "upload_error": error,
619
- "text_index": i
620
- })
621
- else:
622
- # Clean up local file on successful upload
623
- try:
624
- os.remove(output_path)
625
- except:
626
- pass
627
-
628
- results.append({
629
- "status": "success",
630
- "filename": filename,
631
- "oci_path": upload_result.get("path", f"{request.project_id}/voiceover/{filename}"),
632
- "file_size": file_size,
633
- "text_index": i
634
- })
635
-
636
- except Exception as e:
637
- results.append({
638
- "status": "error",
639
- "filename": filename,
640
- "error": str(e),
641
- "text_index": i
642
- })
643
-
644
- # Count successes and errors
645
- success_count = len([r for r in results if r["status"] in ["success", "success_local"]])
646
- error_count = len([r for r in results if r["status"] == "error"])
647
-
648
- return {
649
- "status": "completed",
650
- "message": f"Processed {len(request.texts)} texts: {success_count} successful, {error_count} errors",
651
- "results": results,
652
- "summary": {
653
- "total": len(request.texts),
654
- "successful": success_count,
655
- "errors": error_count
656
- }
657
- }
658
-
659
- except Exception as e:
660
- print(f"❌ Batch TTS error: {str(e)}")
661
- raise HTTPException(status_code=500, detail=str(e))
662
-
663
- @app.post("/api/clone-voice")
664
- async def clone_voice_endpoint(
665
- project_id: str = Form(...),
666
- voice_name: str = Form(...),
667
- description: str = Form(""),
668
- files: List[UploadFile] = File(...)
669
- ):
670
- """Clone a voice from uploaded audio samples"""
671
- try:
672
- if not files:
673
- raise HTTPException(status_code=400, detail="No audio files provided")
674
-
675
- # Save uploaded files temporarily
676
- temp_files = []
677
- for file in files:
678
- if not file.filename.lower().endswith(('.wav', '.mp3', '.flac')):
679
- raise HTTPException(status_code=400, detail="Only WAV, MP3, and FLAC files are supported")
680
-
681
- temp_path = f"/tmp/{uuid.uuid4()}_{file.filename}"
682
- with open(temp_path, "wb") as f:
683
- shutil.copyfileobj(file.file, f)
684
- temp_files.append(temp_path)
685
-
686
- # Clone voice
687
- success, message = clone_voice(voice_name, temp_files, description)
688
-
689
- # Clean up temp files
690
- for temp_file in temp_files:
691
- try:
692
- os.remove(temp_file)
693
- except:
694
- pass
695
-
696
- if success:
697
- return {
698
- "status": "success",
699
- "message": message,
700
- "voice_name": voice_name,
701
- "samples_used": len(temp_files)
702
- }
703
- else:
704
- raise HTTPException(status_code=500, detail=message)
705
-
706
- except HTTPException:
707
- raise
708
- except Exception as e:
709
- print(f"❌ Voice cloning error: {str(e)}")
710
- raise HTTPException(status_code=500, detail=f"Voice cloning failed: {str(e)}")
711
-
712
- @app.get("/api/voices")
713
- async def list_voices():
714
- """List all available cloned voices"""
715
- try:
716
- voices_dir = Path("/tmp/voices")
717
- if not voices_dir.exists():
718
- return {"voices": []}
719
-
720
- voices = []
721
- for voice_dir in voices_dir.iterdir():
722
- if voice_dir.is_dir():
723
- samples = list(voice_dir.glob("sample_*.wav"))
724
- voices.append({
725
- "name": voice_dir.name,
726
- "samples_count": len(samples),
727
- "created_at": datetime.fromtimestamp(voice_dir.stat().st_mtime).isoformat()
728
- })
729
-
730
- return {"voices": voices}
731
- except Exception as e:
732
- raise HTTPException(status_code=500, detail=f"Failed to list voices: {str(e)}")
733
-
734
- @app.post("/api/change-voice")
735
- async def change_voice_style(request: ChangeVoiceRequest):
736
- """Change the voice style (reloads model)"""
737
- try:
738
- global model_loaded
739
-
740
- print(f"🔄 Changing voice style to: {request.voice_style}")
741
-
742
- # Reset model loaded flag to force reload
743
- model_loaded = False
744
-
745
- if load_tts_model(request.voice_style):
746
- return {
747
- "status": "success",
748
- "message": f"Voice style changed to {request.voice_style}",
749
- "current_voice_style": current_voice_style,
750
- "current_model": current_model
751
- }
752
- else:
753
- raise HTTPException(status_code=500, detail="Failed to load new voice style")
754
-
755
- except Exception as e:
756
- raise HTTPException(status_code=500, detail=str(e))
757
-
758
  @app.get("/api/voice-styles")
759
  async def get_voice_styles():
760
  """Get available voice styles"""
761
  styles = {
762
  "default_female": "Default female voice (Tacotron2) - Fast",
763
- "clear_male": "Clear male voice (Tacotron2) - Fast",
764
- "male_deep": "Deep male voice (Fallback to default)",
765
- "male_medium": "Medium male voice (Fallback to default)",
766
- "female_1": "Female voice 1 (Fallback to default)",
767
- "female_2": "Female voice 2 (Fallback to default)"
768
  }
769
  return {"voice_styles": styles}
770
 
@@ -775,12 +454,10 @@ async def get_status():
775
  "status": "running",
776
  "model_loaded": model_loaded,
777
  "current_model": current_model if model_loaded else "none",
778
- "current_voice_style": current_voice_style,
779
  "device": DEVICE,
780
  "oci_configured": bool(OCI_UPLOAD_API_URL),
781
  "startup_time": app_startup_time.isoformat(),
782
- "uptime": str(datetime.now() - app_startup_time),
783
- "model_load_attempts": model_load_attempts
784
  }
785
 
786
  # Startup event - NO MODEL LOADING to avoid timeouts
 
19
  os.makedirs("/tmp/output", exist_ok=True)
20
 
21
  # Initialize FastAPI app
22
+ app = FastAPI(
23
+ title="TTS API",
24
+ description="API for text-to-speech with Coqui TTS",
25
+ docs_url="/",
26
+ redoc_url=None
27
+ )
28
 
29
  # Add CORS middleware
30
  app.add_middleware(
 
35
  allow_headers=["*"],
36
  )
37
 
38
+ # Configuration
39
  OCI_UPLOAD_API_URL = os.getenv("OCI_UPLOAD_API_URL", "").strip()
40
  if OCI_UPLOAD_API_URL:
 
41
  OCI_UPLOAD_API_URL = OCI_UPLOAD_API_URL.rstrip('/')
42
 
43
  DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
 
49
  tts = None
50
  model_loaded = False
51
  current_model = ""
 
52
  model_loading = False
 
 
53
  app_startup_time = datetime.now()
54
 
55
  # Pydantic models
 
67
  language: Optional[str] = "en"
68
  voice_style: Optional[str] = "default_female"
69
 
 
 
 
 
 
 
 
 
70
  # Helper functions
71
  def clean_text(text):
72
  """Clean text for TTS generation"""
 
99
  def upload_to_oci(file_path: str, filename: str, project_id: str, file_type="voiceover"):
100
  """Upload file to OCI using your existing API with subfolder support"""
101
  try:
 
102
  if not OCI_UPLOAD_API_URL:
103
  print("⚠️ OCI upload skipped - OCI_UPLOAD_API_URL not configured")
104
  return {"status": "skipped", "message": "OCI upload disabled"}, None
105
 
 
106
  if not OCI_UPLOAD_API_URL.startswith(('http://', 'https://')):
107
  return None, f"Invalid OCI URL format: {OCI_UPLOAD_API_URL}"
108
 
 
148
  print(f"❌ {error_msg}")
149
  return None, error_msg
150
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  except Exception as e:
152
+ error_msg = f"Upload error: {str(e)}"
153
  print(f"❌ {error_msg}")
154
  return None, error_msg
155
 
156
+ def upload_to_oci_with_retry(file_path: str, filename: str, project_id: str, file_type="voiceover", max_retries=2):
157
  """Upload file to OCI with retry logic"""
158
  for attempt in range(max_retries):
159
  try:
 
182
 
183
  return None, "Upload failed: unexpected error"
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  def save_wav(audio, file_path):
186
  """Save audio to WAV file manually"""
187
  try:
 
216
 
217
  def load_tts_model(voice_style="default_female"):
218
  """Load TTS model with different voice options - LAZY LOADING"""
219
+ global tts, model_loaded, current_model, model_loading
220
 
221
  if model_loading:
222
  print("⏳ Model is already being loaded...")
223
  return False
224
 
225
+ if model_loaded:
226
+ print("✅ Model already loaded")
227
  return True
228
 
229
  model_loading = True
 
230
 
231
  try:
232
  from TTS.api import TTS
233
 
234
+ # Use only fast, lightweight models
235
  model_options = {
236
  "default_female": {
237
  "name": "tts_models/en/ljspeech/tacotron2-DDC",
238
  "description": "Tacotron2 - Default female (fast)",
 
239
  },
240
  "clear_male": {
241
  "name": "tts_models/en/ek1/tacotron2",
242
  "description": "Tacotron2 - Clear male voice",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
  }
245
 
246
  selected_model = model_options.get(voice_style, model_options["default_female"])
 
247
 
248
  print(f"🚀 Loading {selected_model['description']}...")
249
+ print("📥 Downloading model (this may take a few minutes on first load)...")
250
 
251
  # Load the selected model
252
  tts = TTS(selected_model["name"]).to(DEVICE)
 
266
 
267
  model_loaded = True
268
  current_model = selected_model["name"]
 
 
269
  return True
270
 
271
  except Exception as e:
 
276
  model_loading = False
277
 
278
  # Health check endpoints - CRITICAL FOR DEPLOYMENT
 
 
 
 
 
 
 
 
 
 
 
 
279
  @app.get("/health")
280
  async def health_check():
281
  """Health check endpoint - must respond quickly"""
 
309
  }
310
 
311
  try:
 
312
  test_url = f"{OCI_UPLOAD_API_URL}/api/health"
313
  response = requests.get(test_url, timeout=5)
314
 
 
337
  """Generate TTS for a single text with lazy model loading"""
338
  try:
339
  # Lazy load model on first request
340
+ if not model_loaded:
341
  print("🔄 Lazy loading TTS model...")
342
  if not load_tts_model(request.voice_style):
343
  return {
 
351
  print(f" Text length: {len(request.text)} characters")
352
  print(f" Voice style: {request.voice_style}")
353
 
 
 
 
 
 
 
 
 
354
  # Generate unique filename
355
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
356
  filename = f"voiceover_{timestamp}.wav"
 
359
  # Ensure output directory exists
360
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
361
 
 
 
 
 
 
 
 
 
 
 
362
  print(f"🔊 Generating TTS to: {output_path}")
363
 
364
  # Clean the text before generation
 
438
  print(f"❌ TTS generation error: {str(e)}")
439
  raise HTTPException(status_code=500, detail=f"TTS generation failed: {str(e)}")
440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  @app.get("/api/voice-styles")
442
  async def get_voice_styles():
443
  """Get available voice styles"""
444
  styles = {
445
  "default_female": "Default female voice (Tacotron2) - Fast",
446
+ "clear_male": "Clear male voice (Tacotron2) - Fast"
 
 
 
 
447
  }
448
  return {"voice_styles": styles}
449
 
 
454
  "status": "running",
455
  "model_loaded": model_loaded,
456
  "current_model": current_model if model_loaded else "none",
 
457
  "device": DEVICE,
458
  "oci_configured": bool(OCI_UPLOAD_API_URL),
459
  "startup_time": app_startup_time.isoformat(),
460
+ "uptime": str(datetime.now() - app_startup_time)
 
461
  }
462
 
463
  # Startup event - NO MODEL LOADING to avoid timeouts