Spaces:
Running
Running
| # app.py - Simple FastAPI-only version for Hugging Face Spaces | |
| import os | |
| import logging | |
| import tempfile | |
| import subprocess | |
| from pathlib import Path | |
| import requests | |
| import uvicorn | |
| from fastapi import FastAPI, HTTPException, Request | |
| from fastapi.responses import FileResponse, HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| class PiperTTSSpaces: | |
| """Piper TTS for Hugging Face Spaces with FastAPI""" | |
| def __init__(self): | |
| self.model_path = self._setup_model() | |
| def _setup_model(self) -> str: | |
| """Download and setup Piper model for Spaces""" | |
| model_dir = Path("./models") | |
| model_dir.mkdir(exist_ok=True) | |
| model_file = model_dir / "en_US-lessac-medium.onnx" | |
| config_file = model_dir / "en_US-lessac-medium.onnx.json" | |
| # Download model if not exists | |
| if not model_file.exists(): | |
| logger.info("Downloading Piper model...") | |
| try: | |
| # Download model | |
| model_url = "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/lessac/medium/en_US-lessac-medium.onnx" | |
| response = requests.get(model_url, stream=True) | |
| response.raise_for_status() | |
| with open(model_file, 'wb') as f: | |
| for chunk in response.iter_content(chunk_size=8192): | |
| f.write(chunk) | |
| # Download config | |
| config_url = "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json" | |
| response = requests.get(config_url) | |
| response.raise_for_status() | |
| with open(config_file, 'w') as f: | |
| f.write(response.text) | |
| logger.info("Model downloaded successfully") | |
| except Exception as e: | |
| logger.error(f"Failed to download model: {e}") | |
| raise | |
| return str(model_file) | |
| def synthesize_to_file(self, text: str) -> str: | |
| """Synthesize text to temporary WAV file""" | |
| try: | |
| # Create temporary file | |
| temp_file = tempfile.NamedTemporaryFile(suffix='.wav', delete=False) | |
| temp_file.close() | |
| # Prepare Piper command | |
| cmd = [ | |
| "python", "-m", "piper", | |
| "--model", self.model_path, | |
| "--output_file", temp_file.name | |
| ] | |
| logger.info(f"TTS request: '{text[:50]}...'") | |
| # Run Piper with text input | |
| process = subprocess.run( | |
| cmd, | |
| input=text.encode('utf-8'), | |
| capture_output=True, | |
| timeout=30 | |
| ) | |
| if process.returncode != 0: | |
| error_msg = process.stderr.decode() if process.stderr else "Unknown error" | |
| logger.error(f"Piper failed: {error_msg}") | |
| raise RuntimeError(f"TTS failed: {error_msg}") | |
| return temp_file.name | |
| except subprocess.TimeoutExpired: | |
| logger.error("TTS generation timed out") | |
| raise RuntimeError("TTS generation timed out") | |
| except Exception as e: | |
| logger.error(f"Synthesis failed: {e}") | |
| raise | |
| # Initialize TTS engine | |
| logger.info("Initializing TTS engine...") | |
| try: | |
| tts_engine = PiperTTSSpaces() | |
| logger.info("TTS engine initialized successfully") | |
| except Exception as e: | |
| logger.error(f"Failed to initialize TTS engine: {e}") | |
| tts_engine = None | |
| # Create FastAPI app | |
| app = FastAPI( | |
| title="Piper TTS API", | |
| description="High-quality neural TTS for digital companions", | |
| version="1.0.0" | |
| ) | |
| # Add CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| async def root(): | |
| """Serve a simple HTML interface""" | |
| return HTMLResponse(""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>🎙️ Piper TTS API</title> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, sans-serif; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: white; | |
| } | |
| .container { | |
| background: rgba(255, 255, 255, 0.1); | |
| padding: 30px; | |
| border-radius: 15px; | |
| backdrop-filter: blur(10px); | |
| box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37); | |
| } | |
| .header { text-align: center; margin-bottom: 30px; } | |
| .form-group { margin: 20px 0; } | |
| label { display: block; margin-bottom: 8px; font-weight: bold; } | |
| textarea { | |
| width: 100%; | |
| padding: 12px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| background: rgba(255, 255, 255, 0.9); | |
| color: #333; | |
| resize: vertical; | |
| } | |
| button { | |
| background: #4CAF50; | |
| color: white; | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| margin: 5px; | |
| } | |
| button:hover { background: #45a049; } | |
| button:disabled { background: #cccccc; cursor: not-allowed; } | |
| .status { | |
| margin: 15px 0; | |
| padding: 10px; | |
| border-radius: 5px; | |
| background: rgba(255, 255, 255, 0.1); | |
| } | |
| .success { background: rgba(76, 175, 80, 0.3); } | |
| .error { background: rgba(244, 67, 54, 0.3); } | |
| audio { width: 100%; margin: 10px 0; } | |
| .examples { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 10px; | |
| margin: 20px 0; | |
| } | |
| .example-btn { | |
| background: rgba(255, 255, 255, 0.2); | |
| padding: 8px 12px; | |
| font-size: 14px; | |
| } | |
| .api-info { | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin: 20px 0; | |
| } | |
| code { | |
| background: rgba(0, 0, 0, 0.3); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-family: monospace; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🎙️ Piper TTS API</h1> | |
| <p>High-quality neural text-to-speech for digital companions</p> | |
| </div> | |
| <div class="form-group"> | |
| <label for="textInput">💬 Enter text to convert to speech:</label> | |
| <textarea id="textInput" rows="4" placeholder="Hello! I'm your digital companion powered by Piper TTS.">Hello! I'm your digital companion powered by Piper TTS. I can generate natural-sounding speech perfect for conversational AI applications.</textarea> | |
| </div> | |
| <div class="form-group"> | |
| <button onclick="generateSpeech()" id="generateBtn">🎵 Generate Speech</button> | |
| <button onclick="clearAll()">🗑️ Clear</button> | |
| </div> | |
| <div class="examples"> | |
| <button class="example-btn" onclick="setExample('Hello! How can I help you today?')">Greeting</button> | |
| <button class="example-btn" onclick="setExample('I understand your question perfectly.')">Understanding</button> | |
| <button class="example-btn" onclick="setExample('Let me think about that for a moment.')">Thinking</button> | |
| <button class="example-btn" onclick="setExample('Thank you for using our service!')">Thanks</button> | |
| <button class="example-btn" onclick="setExample('Is there anything else you need help with?')">Follow-up</button> | |
| <button class="example-btn" onclick="setExample('I apologize for any confusion.')">Apology</button> | |
| </div> | |
| <div id="status" class="status" style="display: none;"></div> | |
| <audio id="audioPlayer" controls style="display: none;"></audio> | |
| <div class="api-info"> | |
| <h3>🚀 API Integration</h3> | |
| <p><strong>Endpoint:</strong> <code>POST /api/tts</code></p> | |
| <p><strong>Body:</strong> <code>{"text": "Your text here"}</code></p> | |
| <p><strong>Response:</strong> WAV audio file</p> | |
| <h4>Python Example:</h4> | |
| <pre><code>import requests | |
| response = requests.post( | |
| "https://eshwar06-piper-tts-server.hf.space/api/tts", | |
| json={"text": "Hello from Python!"} | |
| ) | |
| with open("speech.wav", "wb") as f: | |
| f.write(response.content)</code></pre> | |
| <h4>cURL Example:</h4> | |
| <pre><code>curl -X POST "https://eshwar06-piper-tts-server.hf.space/api/tts" \\ | |
| -H "Content-Type: application/json" \\ | |
| -d '{"text":"Hello world!"}' \\ | |
| --output speech.wav</code></pre> | |
| </div> | |
| </div> | |
| <script> | |
| function setExample(text) { | |
| document.getElementById('textInput').value = text; | |
| } | |
| function clearAll() { | |
| document.getElementById('textInput').value = ''; | |
| document.getElementById('status').style.display = 'none'; | |
| document.getElementById('audioPlayer').style.display = 'none'; | |
| } | |
| async function generateSpeech() { | |
| const text = document.getElementById('textInput').value; | |
| const statusDiv = document.getElementById('status'); | |
| const audioPlayer = document.getElementById('audioPlayer'); | |
| const generateBtn = document.getElementById('generateBtn'); | |
| if (!text.trim()) { | |
| showStatus('Please enter some text', 'error'); | |
| return; | |
| } | |
| generateBtn.disabled = true; | |
| generateBtn.textContent = '⏳ Generating...'; | |
| showStatus('Generating speech, please wait...', 'info'); | |
| try { | |
| const response = await fetch('/api/tts', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({text: text}) | |
| }); | |
| if (response.ok) { | |
| const audioBlob = await response.blob(); | |
| const audioUrl = URL.createObjectURL(audioBlob); | |
| audioPlayer.src = audioUrl; | |
| audioPlayer.style.display = 'block'; | |
| showStatus('✅ Speech generated successfully! Click play below.', 'success'); | |
| } else { | |
| const errorText = await response.text(); | |
| showStatus(`❌ Error: ${response.status} - ${errorText}`, 'error'); | |
| } | |
| } catch (error) { | |
| showStatus(`❌ Error: ${error.message}`, 'error'); | |
| } finally { | |
| generateBtn.disabled = false; | |
| generateBtn.textContent = '🎵 Generate Speech'; | |
| } | |
| } | |
| function showStatus(message, type) { | |
| const statusDiv = document.getElementById('status'); | |
| statusDiv.textContent = message; | |
| statusDiv.className = `status ${type}`; | |
| statusDiv.style.display = 'block'; | |
| } | |
| // Allow Enter key to generate speech | |
| document.getElementById('textInput').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter' && e.ctrlKey) { | |
| generateSpeech(); | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """) | |
| async def generate_tts(request: dict): | |
| """ | |
| Generate TTS from text | |
| Body: {"text": "Your text here"} | |
| Returns: WAV audio file | |
| """ | |
| text = request.get("text", "") | |
| if not text or not text.strip(): | |
| raise HTTPException(status_code=400, detail="Text is required") | |
| if len(text) > 1000: | |
| raise HTTPException(status_code=400, detail="Text too long (max 1000 characters)") | |
| if not tts_engine: | |
| raise HTTPException(status_code=503, detail="TTS engine not available") | |
| try: | |
| audio_file = tts_engine.synthesize_to_file(text) | |
| return FileResponse( | |
| audio_file, | |
| media_type="audio/wav", | |
| filename="speech.wav", | |
| background=lambda: os.unlink(audio_file) if os.path.exists(audio_file) else None | |
| ) | |
| except Exception as e: | |
| logger.error(f"TTS failed: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def health_check(): | |
| """Health check endpoint""" | |
| return { | |
| "status": "healthy" if tts_engine else "tts_engine_unavailable", | |
| "service": "Piper TTS", | |
| "model_loaded": tts_engine is not None, | |
| "version": "1.0.0" | |
| } | |
| async def docs_redirect(): | |
| """Redirect to API docs""" | |
| return {"message": "Visit /docs for interactive API documentation"} | |
| # Health check for Spaces | |
| async def health_alias(): | |
| """Alternative health endpoint""" | |
| return await health_check() | |
| if __name__ == "__main__": | |
| # Run the FastAPI app | |
| uvicorn.run( | |
| app, | |
| host="0.0.0.0", | |
| port=7860, # Spaces default port | |
| log_level="info" | |
| ) |