Spaces:
Running
Running
| from flask import Flask, request, jsonify, send_file | |
| import edge_tts | |
| import asyncio | |
| import tempfile | |
| import os | |
| app = Flask(__name__) | |
| # --------------------------- | |
| # Async helpers | |
| # --------------------------- | |
| async def get_voices_async(): | |
| voices = await edge_tts.list_voices() | |
| return [ | |
| { | |
| "short_name": v["ShortName"], | |
| "locale": v["Locale"], | |
| "gender": v["Gender"], | |
| "display": f"{v['ShortName']} - {v['Locale']} ({v['Gender']})" | |
| } | |
| for v in voices | |
| ] | |
| async def tts_async(text, voice, rate, pitch): | |
| rate_str = f"{rate:+d}%" | |
| pitch_str = f"{pitch:+d}Hz" | |
| communicate = edge_tts.Communicate( | |
| text=text, | |
| voice=voice, | |
| rate=rate_str, | |
| pitch=pitch_str | |
| ) | |
| tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") | |
| tmp_path = tmp_file.name | |
| tmp_file.close() | |
| await communicate.save(tmp_path) | |
| return tmp_path | |
| # --------------------------- | |
| # Routes | |
| # --------------------------- | |
| def hello(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Edge TTS Voices</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| background-color: #111; | |
| color: #fff; | |
| padding: 40px; | |
| } | |
| h1 { | |
| text-align: center; | |
| } | |
| .stats { | |
| display: flex; | |
| justify-content: center; | |
| gap: 40px; | |
| margin-bottom: 30px; | |
| } | |
| .card { | |
| background: #1e1e1e; | |
| padding: 20px; | |
| border-radius: 10px; | |
| text-align: center; | |
| min-width: 150px; | |
| } | |
| .male { border: 2px solid #3498db; } | |
| .female { border: 2px solid #e91e63; } | |
| .total { border: 2px solid #2ecc71; } | |
| .voices-container { | |
| display: flex; | |
| gap: 40px; | |
| } | |
| .voices-list { | |
| flex: 1; | |
| background: #1a1a1a; | |
| padding: 20px; | |
| border-radius: 10px; | |
| max-height: 500px; | |
| overflow-y: auto; | |
| } | |
| .voice-item { | |
| padding: 6px 0; | |
| border-bottom: 1px solid #333; | |
| font-size: 14px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>π Edge TTS Voices</h1> | |
| <div class="stats"> | |
| <div class="card total"> | |
| <h2 id="totalCount">0</h2> | |
| <p>Total Voices</p> | |
| </div> | |
| <div class="card male"> | |
| <h2 id="maleCount">0</h2> | |
| <p>Male Voices</p> | |
| </div> | |
| <div class="card female"> | |
| <h2 id="femaleCount">0</h2> | |
| <p>Female Voices</p> | |
| </div> | |
| </div> | |
| <div class="voices-container"> | |
| <div class="voices-list"> | |
| <h3>π¨ Male Voices</h3> | |
| <div id="maleVoices"></div> | |
| </div> | |
| <div class="voices-list"> | |
| <h3>π© Female Voices</h3> | |
| <div id="femaleVoices"></div> | |
| </div> | |
| </div> | |
| <script> | |
| async function loadVoices() { | |
| try { | |
| const response = await fetch("/voices"); | |
| const data = await response.json(); | |
| const maleVoices = data.Male.voices; | |
| const femaleVoices = data.Female.voices; | |
| const maleCount = data.Male.count; | |
| const femaleCount = data.Female.count; | |
| const total = maleCount + femaleCount; | |
| document.getElementById("maleCount").innerText = maleCount; | |
| document.getElementById("femaleCount").innerText = femaleCount; | |
| document.getElementById("totalCount").innerText = total; | |
| const maleContainer = document.getElementById("maleVoices"); | |
| const femaleContainer = document.getElementById("femaleVoices"); | |
| maleVoices.forEach(v => { | |
| const div = document.createElement("div"); | |
| div.className = "voice-item"; | |
| div.innerText = v.short_name; | |
| maleContainer.appendChild(div); | |
| }); | |
| femaleVoices.forEach(v => { | |
| const div = document.createElement("div"); | |
| div.className = "voice-item"; | |
| div.innerText = v.short_name; | |
| femaleContainer.appendChild(div); | |
| }); | |
| } catch (error) { | |
| alert("Error loading voices: " + error); | |
| } | |
| } | |
| loadVoices(); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def voices(): | |
| voices = asyncio.run(get_voices_async()) | |
| grouped = { | |
| "Male": { | |
| "count": 0, | |
| "voices": [] | |
| }, | |
| "Female": { | |
| "count": 0, | |
| "voices": [] | |
| } | |
| } | |
| for v in voices: | |
| gender = v["gender"].lower() | |
| if gender == "male": | |
| grouped["Male"]["voices"].append(v) | |
| grouped["Male"]["count"] += 1 | |
| elif gender == "female": | |
| grouped["Female"]["voices"].append(v) | |
| grouped["Female"]["count"] += 1 | |
| return jsonify(grouped) | |
| def tts(): | |
| data = request.json | |
| text = data.get("text", "").strip() | |
| voice = data.get("voice") | |
| rate = int(data.get("rate", 0)) | |
| pitch = int(data.get("pitch", 0)) | |
| if not text: | |
| return jsonify({"error": "Text is required"}), 400 | |
| if not voice: | |
| return jsonify({"error": "Voice is required"}), 400 | |
| try: | |
| audio_path = asyncio.run(tts_async(text, voice, rate, pitch)) | |
| return send_file( | |
| audio_path, | |
| mimetype="audio/mpeg", | |
| as_attachment=True, | |
| download_name="tts.mp3" | |
| ) | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| # --------------------------- | |
| # Run server | |
| # --------------------------- | |
| if __name__ == "__main__": | |
| app.run( | |
| host="0.0.0.0", | |
| port=7860, | |
| debug=False | |
| ) | |