feat: 한국어 발음 개선 전처리(기호/영어약어/사용자사전)
- g2pkk 한계로 뭉개지는 약어·기호를 g2p 이전에 한글로 교정 - 예: GPU→지피유, API→에이피아이, 50%→50퍼센트, 25℃→25도 - USER_DICT로 특정 단어 발음 교정 확장 가능 - AUTO/KR 모드에서 적용(영어 전용 모드는 미적용) - 참고: 속도 뭉개짐과는 별개(g2p/정규화 문제)
This commit is contained in:
97
app/engines/ko_normalize.py
Normal file
97
app/engines/ko_normalize.py
Normal file
@@ -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": "에이에스에이피",
|
||||
}
|
||||
|
||||
# 사용자 발음 교정 사전 (문제 단어를 여기에 추가; 한글→한글도 가능)
|
||||
# 예: "육개장": "육깨장", "<22>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"(?<![A-Za-z0-9])" + re.escape(k) + r"(?![A-Za-z0-9])"
|
||||
text = re.sub(pattern, mapping[k], text, flags=flags)
|
||||
return text
|
||||
|
||||
|
||||
def normalize_korean(text: str) -> 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
|
||||
Reference in New Issue
Block a user