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