test: add MeloTTS-Korean browser test harness
melo-test/ contains a minimal FastAPI server + static HTML UI for auditioning MeloTTS-Korean before wiring it into the bot: - server.py: POST /tts -> wav, model loaded once at startup - index.html: textarea + presets + audio player, shows synth latency - requirements.txt / README.md: setup steps Directory is melo-test/ because the repo .gitignore already excludes test/. Discord-bot integration is unchanged in this branch; this is a local audition harness only. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
104
melo-test/server.py
Normal file
104
melo-test/server.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
MeloTTS-Korean 테스트 서버.
|
||||
|
||||
POST /tts { "text": "...", "speed": 1.0 } -> audio/wav
|
||||
GET / -> index.html (테스트 UI)
|
||||
|
||||
실행: python server.py (기본 0.0.0.0:7860)
|
||||
"""
|
||||
import os
|
||||
import io
|
||||
import time
|
||||
import tempfile
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
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
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
log = logging.getLogger("melo-test")
|
||||
|
||||
# MeloTTS 모델 핸들 (lifespan 에서 로드)
|
||||
state = {"model": None, "speaker_id": None}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_: FastAPI):
|
||||
# import 가 무거우므로 lifespan 안에서 한 번만
|
||||
from melo.api import TTS
|
||||
log.info("MeloTTS-Korean 모델 로딩중... (첫 실행은 가중치 다운로드로 수 분 걸릴 수 있음)")
|
||||
t0 = time.time()
|
||||
device = os.environ.get("MELO_DEVICE", "auto")
|
||||
model = TTS(language="KR", device=device)
|
||||
spk = model.hps.data.spk2id
|
||||
# 한국어 모델은 보통 {"KR": 0}
|
||||
speaker_id = spk.get("KR", next(iter(spk.values())))
|
||||
state["model"] = model
|
||||
state["speaker_id"] = speaker_id
|
||||
log.info("모델 로딩 완료 (%.1fs). speakers=%s, 사용=%s",
|
||||
time.time() - t0, list(spk.keys()), speaker_id)
|
||||
yield
|
||||
state["model"] = None
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
class TTSReq(BaseModel):
|
||||
text: str
|
||||
speed: float = 1.0
|
||||
|
||||
|
||||
@app.post("/tts")
|
||||
def tts(req: TTSReq):
|
||||
model = state.get("model")
|
||||
if model is None:
|
||||
raise HTTPException(503, "model not ready")
|
||||
text = (req.text or "").strip()
|
||||
if not text:
|
||||
raise HTTPException(400, "empty text")
|
||||
if len(text) > 500:
|
||||
raise HTTPException(400, "too long (<=500)")
|
||||
|
||||
speed = max(0.5, min(2.0, float(req.speed or 1.0)))
|
||||
fd, out_path = tempfile.mkstemp(suffix=".wav")
|
||||
os.close(fd)
|
||||
try:
|
||||
t0 = time.time()
|
||||
model.tts_to_file(text, state["speaker_id"], out_path, speed=speed)
|
||||
dt_ms = int((time.time() - t0) * 1000)
|
||||
with open(out_path, "rb") as f:
|
||||
buf = f.read()
|
||||
log.info("tts ok: %dms, %dB, text=%r", dt_ms, len(buf), text[:30])
|
||||
return Response(
|
||||
content=buf,
|
||||
media_type="audio/wav",
|
||||
headers={"X-Synth-Ms": str(dt_ms), "Cache-Control": "no-store"},
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
os.unlink(out_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# 정적 파일 (index.html) 서빙: 반드시 라우트 등록 뒤에 mount
|
||||
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", "7860"))
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
Reference in New Issue
Block a user