Pure-Python alternative to melo-test/ when the MeloTTS native build chain (MeCab/Rust/fugashi/mecab-python3/tokenizers) refuses to install. - edge-test/server.py: FastAPI + edge_tts.Communicate.stream() -> mp3, 9 Korean Neural voices, rate/pitch knobs. - edge-test/index.html: voice selector + rate/pitch + presets + audio. - requirements.txt: edge-tts + fastapi + uvicorn. No native deps. - README: comparison table vs MeloTTS, setup, caveat that edge-tts is an unofficial wrapper over the Edge "Read Aloud" endpoint. Default port 7861 so it coexists with melo-test/ (7860). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
3.1 KiB
Python
101 lines
3.1 KiB
Python
"""
|
|
Microsoft Edge TTS 한국어 테스트 서버.
|
|
|
|
요청한 텍스트를 Microsoft Edge 의 Read Aloud(Neural) 서비스로 합성.
|
|
네이티브 빌드 의존성 0 — PyTorch / MeCab / Rust / GPU 모두 불필요.
|
|
순수 Python 패키지(edge-tts + fastapi + uvicorn) 만으로 동작.
|
|
|
|
POST /tts { "text": "...", "voice": "ko-KR-SunHiNeural", "rate": "+0%", "pitch": "+0Hz" } -> audio/mpeg
|
|
GET /voices -> 사용 가능한 한국어 보이스 목록
|
|
GET / -> index.html
|
|
"""
|
|
import os
|
|
import time
|
|
import logging
|
|
from io import BytesIO
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import Response
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
import edge_tts
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
log = logging.getLogger("edge-test")
|
|
|
|
# Microsoft Azure Neural Voices (Korean). 실제 가용 목록은 /voices 가 동적으로 반환.
|
|
KOREAN_VOICES = [
|
|
("ko-KR-SunHiNeural", "여 · 차분"),
|
|
("ko-KR-InJoonNeural", "남 · 표준"),
|
|
("ko-KR-BongJinNeural", "남 · 중후"),
|
|
("ko-KR-GookMinNeural", "남 · 친근"),
|
|
("ko-KR-HyunsuMultilingualNeural", "남 · 다국어"),
|
|
("ko-KR-JiMinNeural", "여 · 밝음"),
|
|
("ko-KR-SeoHyeonNeural", "여 · 차분"),
|
|
("ko-KR-SoonBokNeural", "여 · 시니어"),
|
|
("ko-KR-YuJinNeural", "여 · 표준"),
|
|
]
|
|
|
|
app = FastAPI()
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
|
|
class TTSReq(BaseModel):
|
|
text: str
|
|
voice: str = "ko-KR-SunHiNeural"
|
|
rate: str = "+0%" # 예: "-10%", "+20%"
|
|
pitch: str = "+0Hz" # 예: "-50Hz", "+30Hz"
|
|
|
|
|
|
@app.get("/voices")
|
|
def voices():
|
|
return {"voices": [{"name": n, "label": lbl} for n, lbl in KOREAN_VOICES]}
|
|
|
|
|
|
@app.post("/tts")
|
|
async def tts(req: TTSReq):
|
|
text = (req.text or "").strip()
|
|
if not text:
|
|
raise HTTPException(400, "empty text")
|
|
if len(text) > 1000:
|
|
raise HTTPException(400, "too long (<=1000)")
|
|
voice = req.voice or "ko-KR-SunHiNeural"
|
|
|
|
t0 = time.time()
|
|
buf = BytesIO()
|
|
try:
|
|
communicate = edge_tts.Communicate(text, voice, rate=req.rate, pitch=req.pitch)
|
|
async for chunk in communicate.stream():
|
|
if chunk["type"] == "audio":
|
|
buf.write(chunk["data"])
|
|
except Exception as e:
|
|
log.error("edge_tts error: %r", e)
|
|
raise HTTPException(502, f"edge_tts failed: {e}")
|
|
|
|
dt_ms = int((time.time() - t0) * 1000)
|
|
data = buf.getvalue()
|
|
if not data:
|
|
raise HTTPException(502, "empty audio from edge_tts")
|
|
log.info("tts ok: %dms, %dB, voice=%s, text=%r", dt_ms, len(data), voice, text[:30])
|
|
return Response(
|
|
content=data,
|
|
media_type="audio/mpeg",
|
|
headers={"X-Synth-Ms": str(dt_ms), "Cache-Control": "no-store"},
|
|
)
|
|
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
app.mount("/", StaticFiles(directory=HERE, html=True), name="static")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
port = int(os.environ.get("PORT", "7861"))
|
|
uvicorn.run(app, host="0.0.0.0", port=port)
|