| FROM python:3.11-slim |
|
|
| WORKDIR /app |
|
|
| RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && \ |
| rm -rf /var/lib/apt/lists/* |
|
|
| RUN pip install --no-cache-dir fastapi uvicorn edge-tts |
|
|
| RUN cat <<'PYEOF' > /app/main.py |
| from fastapi import FastAPI, Query |
| from fastapi.responses import StreamingResponse, HTMLResponse |
| from fastapi.middleware.cors import CORSMiddleware |
| import edge_tts |
|
|
| app = FastAPI() |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| VOICES = [ |
| "en-US-AvaNeural", "en-US-AndrewNeural", "en-GB-SoniaNeural", |
| "en-GB-RyanNeural", "ja-JP-NanamiNeural", "ja-JP-KeitaNeural", |
| "de-DE-KatjaNeural", "fr-FR-DeniseNeural", "es-ES-ElviraNeural", |
| "zh-CN-XiaoxiaoNeural", "zh-CN-YunjianNeural", "ko-KR-SunHiNeural" |
| ] |
|
|
| @app.get("/") |
| async def root(): |
| return {"message": "Edge TTS API", "voices": VOICES} |
|
|
| @app.get("/tts") |
| async def tts( |
| text: str = Query(..., description="Text to speak"), |
| voice: str = Query(default="en-US-AvaNeural", description="Voice ID"), |
| rate: str = Query(default="+0%", description="Speed: +10% or -10%"), |
| pitch: str = Query(default="+0Hz", description="Pitch: +10Hz or -10Hz") |
| ): |
| |
| if not rate.startswith(("+", "-")): |
| rate = f"+{rate}" |
| if not pitch.startswith(("+", "-")): |
| pitch = f"+{pitch}" |
|
|
| communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch) |
| |
| async def audio_generator(): |
| async for chunk in communicate.stream(): |
| if chunk["type"] == "audio": |
| yield chunk["data"] |
|
|
| return StreamingResponse(audio_generator(), media_type="audio/mpeg") |
|
|
| @app.get("/ui", response_class=HTMLResponse) |
| async def ui(): |
| return """<!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="UTF-8"> |
| <title>Edge TTS</title> |
| <style> |
| body{font-family:system-ui;max-width:600px;margin:40px auto;padding:20px;background:#111;color:#eee} |
| textarea,select,button{width:100%;padding:12px;margin:8px 0;border-radius:8px;border:none;font-size:16px} |
| textarea{height:120px;background:#222;color:#fff;resize:vertical} |
| select{background:#222;color:#fff} |
| button{background:#3b82f6;color:#fff;cursor:pointer;font-weight:bold} |
| button:hover{background:#2563eb} |
| button:disabled{opacity:.5;cursor:not-allowed} |
| .row{display:flex;gap:10px} |
| .row>*{flex:1} |
| audio{width:100%;margin-top:15px} |
| #status{text-align:center;margin-top:10px;color:#aaa} |
| </style> |
| </head> |
| <body> |
| <h2>Edge TTS API</h2> |
| <textarea id="text" placeholder="Enter text to speak...">Hello! This is Microsoft Edge TTS running inside Docker.</textarea> |
| <div class="row"> |
| <select id="voice"> |
| <option value="en-US-AvaNeural">Ava (Female)</option> |
| <option value="en-US-AndrewNeural">Andrew (Male)</option> |
| <option value="en-GB-SoniaNeural">Sonia (Female)</option> |
| <option value="en-GB-RyanNeural">Ryan (Male)</option> |
| <option value="ja-JP-NanamiNeural">Nanami (Female)</option> |
| <option value="ja-JP-KeitaNeural">Keita (Male)</option> |
| <option value="de-DE-KatjaNeural">Katja (Female)</option> |
| <option value="fr-FR-DeniseNeural">Denise (Female)</option> |
| <option value="es-ES-ElviraNeural">Elvira (Female)</option> |
| <option value="zh-CN-XiaoxiaoNeural">Xiaoxiao (Female)</option> |
| <option value="zh-CN-YunjianNeural">Yunjian (Male)</option> |
| <option value="ko-KR-SunHiNeural">Sun-Hi (Female)</option> |
| </select> |
| <select id="rate"> |
| <option value="-50%">Very Slow</option> |
| <option value="-20%">Slow</option> |
| <option value="+0%" selected>Normal</option> |
| <option value="+20%">Fast</option> |
| <option value="+50%">Very Fast</option> |
| </select> |
| <select id="pitch"> |
| <option value="-20Hz">Low</option> |
| <option value="+0Hz" selected>Normal</option> |
| <option value="+20Hz">High</option> |
| </select> |
| </div> |
| <button id="btn" onclick="generate()">Generate Speech</button> |
| <div id="status"></div> |
| <audio id="player" controls style="display:none"></audio> |
| <script> |
| async function generate(){ |
| const btn=document.getElementById("btn"),status=document.getElementById("status"),player=document.getElementById("player"); |
| const text=document.getElementById("text").value.trim(); |
| if(!text){status.textContent="Please enter text.";return} |
| btn.disabled=true;status.textContent="Generating..."; |
| const p=new URLSearchParams({text,voice:document.getElementById("voice").value,rate:document.getElementById("rate").value,pitch:document.getElementById("pitch").value}); |
| try{ |
| const res=await fetch("/tts?"+p.toString()); |
| if(!res.ok)throw new Error(await res.text()); |
| const blob=await res.blob(),url=URL.createObjectURL(blob); |
| player.src=url;player.style.display="block";player.play();status.textContent="Done!"; |
| }catch(err){status.textContent="Error: "+err.message} |
| finally{btn.disabled=false} |
| } |
| </script> |
| </body> |
| </html>""" |
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run(app, host="0.0.0.0", port=7860) |
| PYEOF |
|
|
| EXPOSE 7860 |
|
|
| CMD ["python", "/app/main.py"] |