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"]