Peter Michael Gits Claude commited on
Commit
9cd217a
Β·
1 Parent(s): c950a11

feat: Add WebSocket support for real-time TTS integration

Browse files

- Added WebSocketTTSHandler class for real-time TTS synthesis
- Integrated FastAPI WebSocket endpoints (/ws/tts, /ws/tts/{client_id})
- ZeroGPU Bark synthesis with base64 audio response
- Support for multiple voice presets via WebSocket
- Triple protocol support: Gradio + MCP + WebSocket
- Compatible with ChatCal WebRTC voice pipeline

πŸ€– Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +238 -1
app.py CHANGED
@@ -91,6 +91,9 @@ import spaces # Required for ZeroGPU
91
  import asyncio
92
  import threading
93
  import json
 
 
 
94
 
95
  # 6. Additional uvicorn patching after import
96
  try:
@@ -315,10 +318,222 @@ def get_system_info():
315
  "πŸ”§ PyTorch": torch.__version__,
316
  "πŸ€– Model Status": "βœ… Loaded" if model is not None else "⏳ Loading on first use",
317
  "πŸŽ›οΈ Mixed Precision": "βœ… Enabled" if torch.cuda.is_available() else "❌ CPU Mode",
318
- "πŸ”Œ MCP Server": "βœ… Available" if MCP_AVAILABLE else "❌ Not Available"
 
319
  }
320
  return "\n".join([f"{k}: {v}" for k, v in info.items()])
321
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  # MCP Tool Definitions and Handlers
323
  if MCP_AVAILABLE:
324
 
@@ -826,6 +1041,28 @@ def safe_main():
826
  else:
827
  safe_log("error", "❌ MCP not available but MCP-only mode requested")
828
  sys.exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
  else:
830
  # Dual mode - both Gradio and MCP
831
  safe_log("info", "πŸš€ Starting TTS service with dual protocol support...")
 
91
  import asyncio
92
  import threading
93
  import json
94
+ import base64
95
+ from datetime import datetime
96
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
97
 
98
  # 6. Additional uvicorn patching after import
99
  try:
 
318
  "πŸ”§ PyTorch": torch.__version__,
319
  "πŸ€– Model Status": "βœ… Loaded" if model is not None else "⏳ Loading on first use",
320
  "πŸŽ›οΈ Mixed Precision": "βœ… Enabled" if torch.cuda.is_available() else "❌ CPU Mode",
321
+ "πŸ”Œ MCP Server": "βœ… Available" if MCP_AVAILABLE else "❌ Not Available",
322
+ "🌐 WebSocket TTS": "βœ… Ready" if model is not None else "⏳ Initializing"
323
  }
324
  return "\n".join([f"{k}: {v}" for k, v in info.items()])
325
 
326
+ # WebSocket TTS Handler for Real-time Text-to-Speech
327
+ class WebSocketTTSHandler:
328
+ """WebSocket handler for real-time TTS integration with ChatCal WebRTC"""
329
+
330
+ def __init__(self):
331
+ self.active_connections = {}
332
+
333
+ async def connect(self, websocket: WebSocket, client_id: str):
334
+ """Accept WebSocket connection for TTS service"""
335
+ await websocket.accept()
336
+ self.active_connections[client_id] = websocket
337
+ safe_log("info", f"πŸ”Œ TTS WebSocket client {client_id} connected")
338
+
339
+ # Send connection confirmation with service info
340
+ await self.send_message(client_id, {
341
+ "type": "tts_connection_confirmed",
342
+ "client_id": client_id,
343
+ "timestamp": datetime.now().isoformat(),
344
+ "service": "tts-gpu-service",
345
+ "model_status": "βœ… Loaded" if model is not None else "⏳ Loading",
346
+ "zerogpu_status": "βœ… Active" if torch.cuda.is_available() else "❌ Not Available",
347
+ "available_voices": ["v2/en_speaker_6", "v2/en_speaker_9", "v2/en_speaker_3", "v2/en_speaker_1"]
348
+ })
349
+
350
+ async def disconnect(self, client_id: str):
351
+ """Clean up connection"""
352
+ if client_id in self.active_connections:
353
+ del self.active_connections[client_id]
354
+ safe_log("info", f"πŸ”Œ TTS WebSocket client {client_id} disconnected")
355
+
356
+ async def send_message(self, client_id: str, message: dict):
357
+ """Send JSON message to client"""
358
+ if client_id in self.active_connections:
359
+ websocket = self.active_connections[client_id]
360
+ try:
361
+ await websocket.send_text(json.dumps(message))
362
+ except Exception as e:
363
+ safe_log("error", f"Failed to send message to TTS client {client_id}: {e}")
364
+ await self.disconnect(client_id)
365
+
366
+ async def handle_text_synthesis(self, client_id: str, text: str, voice_preset: str = "v2/en_speaker_6"):
367
+ """Process text synthesis with real TTS service"""
368
+ try:
369
+ safe_log("info", f"πŸ”Š TTS: Processing text from {client_id}: {text[:50]}...")
370
+
371
+ # Use the existing ZeroGPU synthesize_speech function
372
+ audio_path, status = synthesize_speech(text, voice_preset)
373
+
374
+ if audio_path and "βœ…" in status:
375
+ # Read the generated audio file
376
+ with open(audio_path, 'rb') as audio_file:
377
+ audio_data = audio_file.read()
378
+
379
+ # Encode audio as base64 for WebSocket transmission
380
+ audio_b64 = base64.b64encode(audio_data).decode('utf-8')
381
+
382
+ # Send successful synthesis
383
+ await self.send_message(client_id, {
384
+ "type": "tts_audio_response",
385
+ "audio_data": audio_b64,
386
+ "audio_format": "wav",
387
+ "text": text,
388
+ "voice_preset": voice_preset,
389
+ "timestamp": datetime.now().isoformat(),
390
+ "audio_size": len(audio_data),
391
+ "status": status
392
+ })
393
+
394
+ safe_log("info", f"πŸ”Š TTS: Audio response sent to {client_id} ({len(audio_data)} bytes)")
395
+
396
+ # Clean up temporary file
397
+ import os
398
+ try:
399
+ os.unlink(audio_path)
400
+ except:
401
+ pass
402
+
403
+ else:
404
+ # Send error message
405
+ await self.send_message(client_id, {
406
+ "type": "tts_error",
407
+ "message": f"TTS synthesis failed: {status}",
408
+ "text": text,
409
+ "timestamp": datetime.now().isoformat()
410
+ })
411
+
412
+ except Exception as e:
413
+ safe_log("error", f"TTS WebSocket error for {client_id}: {e}")
414
+ await self.send_message(client_id, {
415
+ "type": "tts_error",
416
+ "message": f"TTS processing error: {str(e)}",
417
+ "timestamp": datetime.now().isoformat()
418
+ })
419
+
420
+ async def handle_message(self, client_id: str, message_data: dict):
421
+ """Handle different types of WebSocket messages"""
422
+ message_type = message_data.get("type")
423
+
424
+ if message_type == "tts_synthesize":
425
+ # Text-to-speech synthesis request
426
+ text = message_data.get("text", "")
427
+ voice_preset = message_data.get("voice_preset", "v2/en_speaker_6")
428
+
429
+ if text.strip():
430
+ await self.handle_text_synthesis(client_id, text, voice_preset)
431
+ else:
432
+ await self.send_message(client_id, {
433
+ "type": "tts_error",
434
+ "message": "Empty text provided for synthesis",
435
+ "timestamp": datetime.now().isoformat()
436
+ })
437
+
438
+ elif message_type == "tts_get_voices":
439
+ # Request available voice presets
440
+ await self.send_message(client_id, {
441
+ "type": "tts_voices_list",
442
+ "voices": ["v2/en_speaker_6", "v2/en_speaker_9", "v2/en_speaker_3", "v2/en_speaker_1"],
443
+ "timestamp": datetime.now().isoformat()
444
+ })
445
+
446
+ else:
447
+ safe_log("warning", f"Unknown TTS message type from {client_id}: {message_type}")
448
+
449
+ # Global TTS WebSocket handler
450
+ tts_websocket_handler = WebSocketTTSHandler()
451
+
452
+ # FastAPI WebSocket Integration for TTS Service
453
+ def create_tts_fastapi_app():
454
+ """Create FastAPI app with TTS WebSocket endpoint"""
455
+ import uuid
456
+
457
+ app = FastAPI(
458
+ title="TTS GPU Service WebSocket API",
459
+ description="Real-time Text-to-Speech with ZeroGPU acceleration",
460
+ version="1.0.0"
461
+ )
462
+
463
+ @app.websocket("/ws/tts")
464
+ async def tts_websocket_endpoint(websocket: WebSocket):
465
+ """WebSocket endpoint for real-time TTS"""
466
+ client_id = str(uuid.uuid4())
467
+
468
+ try:
469
+ await tts_websocket_handler.connect(websocket, client_id)
470
+
471
+ while True:
472
+ # Receive JSON message from client
473
+ data = await websocket.receive_text()
474
+ try:
475
+ message = json.loads(data)
476
+ await tts_websocket_handler.handle_message(client_id, message)
477
+ except json.JSONDecodeError as e:
478
+ await tts_websocket_handler.send_message(client_id, {
479
+ "type": "tts_error",
480
+ "message": f"Invalid JSON format: {str(e)}",
481
+ "timestamp": datetime.now().isoformat()
482
+ })
483
+
484
+ except WebSocketDisconnect:
485
+ await tts_websocket_handler.disconnect(client_id)
486
+ except Exception as e:
487
+ safe_log("error", f"TTS WebSocket endpoint error: {e}")
488
+ await tts_websocket_handler.disconnect(client_id)
489
+
490
+ @app.websocket("/ws/tts/{client_id}")
491
+ async def tts_websocket_with_id(websocket: WebSocket, client_id: str):
492
+ """WebSocket endpoint with specific client ID"""
493
+ try:
494
+ await tts_websocket_handler.connect(websocket, client_id)
495
+
496
+ while True:
497
+ data = await websocket.receive_text()
498
+ try:
499
+ message = json.loads(data)
500
+ await tts_websocket_handler.handle_message(client_id, message)
501
+ except json.JSONDecodeError as e:
502
+ await tts_websocket_handler.send_message(client_id, {
503
+ "type": "tts_error",
504
+ "message": f"Invalid JSON format: {str(e)}",
505
+ "timestamp": datetime.now().isoformat()
506
+ })
507
+
508
+ except WebSocketDisconnect:
509
+ await tts_websocket_handler.disconnect(client_id)
510
+ except Exception as e:
511
+ safe_log("error", f"TTS WebSocket endpoint error: {e}")
512
+ await tts_websocket_handler.disconnect(client_id)
513
+
514
+ @app.get("/")
515
+ async def tts_root():
516
+ """TTS service status endpoint"""
517
+ return {
518
+ "service": "tts-gpu-service",
519
+ "status": "βœ… Ready" if model is not None else "⏳ Loading",
520
+ "zerogpu": "βœ… Active" if torch.cuda.is_available() else "❌ Not Available",
521
+ "websocket_endpoints": ["/ws/tts", "/ws/tts/{client_id}"],
522
+ "available_voices": ["v2/en_speaker_6", "v2/en_speaker_9", "v2/en_speaker_3", "v2/en_speaker_1"],
523
+ "model": "suno/bark"
524
+ }
525
+
526
+ @app.get("/health")
527
+ async def tts_health():
528
+ """Health check endpoint"""
529
+ return {
530
+ "status": "healthy",
531
+ "model_loaded": model is not None,
532
+ "gpu_available": torch.cuda.is_available()
533
+ }
534
+
535
+ return app
536
+
537
  # MCP Tool Definitions and Handlers
538
  if MCP_AVAILABLE:
539
 
 
1041
  else:
1042
  safe_log("error", "❌ MCP not available but MCP-only mode requested")
1043
  sys.exit(1)
1044
+
1045
+ elif "--websocket" in sys.argv:
1046
+ # WebSocket-only mode - FastAPI with TTS WebSocket endpoints
1047
+ safe_log("info", "🌐 Starting TTS service in WebSocket-only mode...")
1048
+
1049
+ try:
1050
+ import uvicorn
1051
+ fastapi_app = create_tts_fastapi_app()
1052
+ safe_log("info", "βœ… TTS WebSocket Server: Starting on port 7860...")
1053
+
1054
+ uvicorn.run(
1055
+ fastapi_app,
1056
+ host="0.0.0.0",
1057
+ port=7860,
1058
+ log_config=None,
1059
+ access_log=False,
1060
+ log_level="critical"
1061
+ )
1062
+ except Exception as e:
1063
+ safe_log("error", f"Failed to start TTS WebSocket server: {e}")
1064
+ sys.exit(1)
1065
+
1066
  else:
1067
  # Dual mode - both Gradio and MCP
1068
  safe_log("info", "πŸš€ Starting TTS service with dual protocol support...")