File size: 4,966 Bytes
e47a387 5072e87 e47a387 20005ee e47a387 8d32b67 e47a387 8d32b67 e47a387 0d89398 e47a387 0d89398 e47a387 8d32b67 e47a387 8d32b67 e47a387 0d89398 e47a387 0d89398 e47a387 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | 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")
):
# edge-tts regex validation requires a leading "+" or "-" sign
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"] |