test(edge): add Microsoft Edge TTS Korean test harness

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>
This commit is contained in:
Claude Owner
2026-05-26 16:07:27 +09:00
parent c68de5a232
commit d3aeab937b
4 changed files with 349 additions and 0 deletions

100
edge-test/server.py Normal file
View File

@@ -0,0 +1,100 @@
"""
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)