diff --git a/app/engines/ko_normalize.py b/app/engines/ko_normalize.py new file mode 100644 index 0000000..3094c69 --- /dev/null +++ b/app/engines/ko_normalize.py @@ -0,0 +1,97 @@ +"""한국어 발음 개선용 전처리. + +MeloTTS 한국어는 g2pkk(규칙 기반 g2p)를 쓰는데, 영어 약어·기호·일부 단어에서 +발음이 뭉개지거나 틀립니다. 학습 없이 개선하는 실전적 방법은 g2p 이전에 +텍스트를 교정하는 것입니다. + +- SYMBOLS: 기호를 한글로 확장(%, ℃ 등) +- ACRONYMS: 흔한 영어 약어를 한글 음차로(약어를 한국어 음성으로 읽게) +- USER_DICT: 특정 단어의 발음을 교정(사용자가 문제 단어를 추가) — 한글→한글 가능 + +적용은 한국어/자동 모드에서만 한다(영어 전용 모드에는 적용 안 함). +""" +from __future__ import annotations + +import re + +# 기호 확장 (앞뒤 문맥 영향 적은 것만 보수적으로) +SYMBOLS: dict[str, str] = { + "℃": "도", + "°C": "도", + "℉": "화씨", + "%": "퍼센트", + "&": "그리고", + "@": "골뱅이", +} + +# 영어 약어 → 한글 음차 (대문자 형태만 매칭) +ACRONYMS: dict[str, str] = { + "AI": "에이아이", + "API": "에이피아이", + "APP": "앱", + "AR": "에이아르", + "VR": "브이아르", + "QR": "큐아르", + "CPU": "씨피유", + "GPU": "지피유", + "GPT": "지피티", + "PC": "피씨", + "TV": "티비", + "IT": "아이티", + "IoT": "아이오티", + "SNS": "에스엔에스", + "OK": "오케이", + "URL": "유알엘", + "ID": "아이디", + "PDF": "피디에프", + "HTTP": "에이치티티피", + "HTTPS": "에이치티티피에스", + "WiFi": "와이파이", + "WIFI": "와이파이", + "DIY": "디아이와이", + "DNA": "디엔에이", + "LED": "엘이디", + "GPS": "지피에스", + "SMS": "에스엠에스", + "OS": "오에스", + "IP": "아이피", + "DB": "디비", + "UI": "유아이", + "UX": "유엑스", + "TTS": "티티에스", + "CD": "씨디", + "DVD": "디비디", + "USB": "유에스비", + "CEO": "씨이오", + "FAQ": "에프에이큐", + "FYI": "에프와이아이", + "ASAP": "에이에스에이피", +} + +# 사용자 발음 교정 사전 (문제 단어를 여기에 추가; 한글→한글도 가능) +# 예: "육개장": "육깨장", "�its": "..." ← 사용자가 알려주는 단어를 반영 +USER_DICT: dict[str, str] = {} + + +def _sub_word_boundary(text: str, mapping: dict[str, str], case_sensitive: bool) -> str: + if not mapping: + return text + # 라틴/숫자에 인접하지 않은 경우에만 치환 (한글 조사 뒤에도 매칭되도록) + keys = sorted(mapping.keys(), key=len, reverse=True) + flags = 0 if case_sensitive else re.IGNORECASE + for k in keys: + pattern = r"(? str: + """한국어 합성 전에 기호/약어/사용자사전을 적용.""" + if not text: + return text + # 사용자 사전(최우선) → 약어(대문자 정확 매칭) → 기호 + text = _sub_word_boundary(text, USER_DICT, case_sensitive=True) + text = _sub_word_boundary(text, ACRONYMS, case_sensitive=True) + for sym, rep in SYMBOLS.items(): + text = text.replace(sym, rep) + return text diff --git a/app/engines/melo_engine.py b/app/engines/melo_engine.py index 02a100c..e24a4ad 100644 --- a/app/engines/melo_engine.py +++ b/app/engines/melo_engine.py @@ -31,6 +31,7 @@ except Exception: # pragma: no cover from melo.api import TTS from .base import BaseEngine +from .ko_normalize import normalize_korean # 영어 화자(억양) 목록 EN_SPEAKERS = [ @@ -274,6 +275,10 @@ class MeloEngine(BaseEngine): pitch = float(max(-12.0, min(12.0, pitch))) language = (language or "AUTO").upper() + # 한국어 발음 개선: 기호/약어/사용자사전 전처리 (한국어·자동 모드) + if language in ("AUTO", "KR"): + text = normalize_korean(text) + if language == "AUTO": # 영어 구간엔 사용자가 고른 억양, 한국어 구간엔 KR 음성 en_speaker = speaker if (speaker and speaker.startswith("EN")) else "EN-US"