diff --git a/edge-test/README.md b/edge-test/README.md new file mode 100644 index 0000000..0cf14f7 --- /dev/null +++ b/edge-test/README.md @@ -0,0 +1,93 @@ +# Microsoft Edge TTS — 한국어 테스트 환경 + +MeloTTS 의 빌드 의존성(MeCab, Rust, fugashi, mecab-python3, tokenizers)이 +계속 깨질 때 사용할 수 있는 **즉시 실행 가능한 대안**. + +내부적으로 Microsoft Edge 브라우저의 "소리내어 읽기" 가 쓰는 Azure Neural Voice +엔드포인트를 호출합니다(공식 SDK 가 아닌 커뮤니티 라이브러리 `edge-tts`). + +## 왜 이걸 먼저 테스트해보는 게 좋은가 + +| 항목 | MeloTTS-Korean | Edge TTS | +| --- | --- | --- | +| 설치 시간 | 5~15분 + 모델 가중치 수백 MB | 30초, 의존성 4개 | +| 네이티브 빌드 | MeCab + Rust + PyTorch | 없음 | +| Python 버전 | 3.10/3.11 강제 | 3.9+ 어디서나 | +| 자연스러움(한국어) | 좋음 | Azure Neural 그대로, 매우 자연 | +| 비용 | 무료(MIT) | 무료(비공식 Edge 엔드포인트) | +| 지연 | 첫 호출 후 ms 단위 | 200~500ms 정도(네트워크) | +| 보이스 수 | 1 (KR) | 9 (남/여/시니어/다국어) | +| 안정성 위험 | 자가호스팅 = 본인 책임 | MS 가 정책 바꾸면 끊길 수 있음 | + +테스트 우선순위로는 **Edge TTS 를 먼저 들어보고**, 마음에 들면 그대로 채택, +"오프라인/내부망/완전 자체 호스팅" 요구가 있으면 MeloTTS Docker 로 진행하는 흐름을 권합니다. + +## 설치 & 실행 + +```bash +# 1. 가상환경 +python -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate + +# 2. 의존성 (30초) +pip install --upgrade pip +pip install -r requirements.txt + +# 3. 서버 +python server.py +# 포트 변경: PORT=8001 python server.py (기본 7861) +``` + +브라우저: `http://localhost:7861` + +> MeloTTS 테스트(`melo-test/`, 포트 7860)와 동시에 띄울 수 있도록 기본 포트가 다릅니다. + +## API + +```http +POST /tts +Content-Type: application/json + +{ + "text": "안녕하세요.", + "voice": "ko-KR-SunHiNeural", + "rate": "+0%", + "pitch": "+0Hz" +} +``` + +응답: `audio/mpeg` (MP3). 헤더 `X-Synth-Ms` 에 합성 ms. + +```http +GET /voices +``` + +응답: 한국어 보이스 목록(JSON). + +## 사용 가능한 한국어 보이스 + +- `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` — 여, 표준 + +## 주의 + +- `edge-tts` 는 비공식 라이브러리입니다. Microsoft 가 엔드포인트를 바꾸면 끊깁니다(과거 수년간은 안정적이었음). +- 운영 안정성/SLA 가 필요하면 Azure Cognitive Services(유료)로 같은 보이스를 정식 사용할 수 있습니다 — 코드 거의 동일. +- 봇에 붙일 때 디스코드 메시지 전체를 다 보내면 텍스트가 외부 서비스에 흘러갑니다. 사내·민감 채널엔 부적합. + +## 디스코드 봇 연동 (테스트 성공 후) + +`src/utils/tts/Edge.ts` 를 추가하고 `src/classes/TTSClient.ts` 의 import 교체. +`textToSpeech(text): Promise` 시그니처는 `Chzzk.ts` / `Google.ts` 와 동일하게. + +## 참고 + +- edge-tts (Python): https://github.com/rany2/edge-tts +- Azure Neural Voices 목록: https://learn.microsoft.com/azure/ai-services/speech-service/language-support diff --git a/edge-test/index.html b/edge-test/index.html new file mode 100644 index 0000000..9385ddb --- /dev/null +++ b/edge-test/index.html @@ -0,0 +1,151 @@ + + + + + +Edge TTS 한국어 테스트 + + + +

Microsoft Edge TTS — 한국어 테스트

+

+ Microsoft Azure Neural Voice 를 무료로(브라우저 Read Aloud 동일 엔드포인트) 사용. + 네이티브 빌드 의존성 0, 설치 30초. +

+ + + +
+ + + + +
+ +
+ + + +
+ +
+ + 대기중 +
+ + + +
+

+ 엔드포인트: POST /tts, 응답 audio/mpeg, 헤더 X-Synth-Ms=합성 ms. + 보이스 목록: GET /voices. +

+ + + + diff --git a/edge-test/requirements.txt b/edge-test/requirements.txt new file mode 100644 index 0000000..287a9d2 --- /dev/null +++ b/edge-test/requirements.txt @@ -0,0 +1,5 @@ +# 순수 Python 패키지만. 네이티브 빌드 의존성 없음. +edge-tts>=6.1.0 +fastapi>=0.110 +uvicorn[standard]>=0.27 +pydantic>=2.0 diff --git a/edge-test/server.py b/edge-test/server.py new file mode 100644 index 0000000..39645d5 --- /dev/null +++ b/edge-test/server.py @@ -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)