feat: 한국어 발음 개선 전처리(기호/영어약어/사용자사전)

- g2pkk 한계로 뭉개지는 약어·기호를 g2p 이전에 한글로 교정
- 예: GPU→지피유, API→에이피아이, 50%→50퍼센트, 25℃→25도
- USER_DICT로 특정 단어 발음 교정 확장 가능
- AUTO/KR 모드에서 적용(영어 전용 모드는 미적용)
- 참고: 속도 뭉개짐과는 별개(g2p/정규화 문제)
This commit is contained in:
claude
2026-08-24 15:00:57 +09:00
parent 1942a7aecd
commit e6bc6ef2be
2 changed files with 102 additions and 0 deletions

View 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

View File

@@ -31,6 +31,7 @@ except Exception: # pragma: no cover
from melo.api import TTS from melo.api import TTS
from .base import BaseEngine from .base import BaseEngine
from .ko_normalize import normalize_korean
# 영어 화자(억양) 목록 # 영어 화자(억양) 목록
EN_SPEAKERS = [ EN_SPEAKERS = [
@@ -274,6 +275,10 @@ class MeloEngine(BaseEngine):
pitch = float(max(-12.0, min(12.0, pitch))) pitch = float(max(-12.0, min(12.0, pitch)))
language = (language or "AUTO").upper() language = (language or "AUTO").upper()
# 한국어 발음 개선: 기호/약어/사용자사전 전처리 (한국어·자동 모드)
if language in ("AUTO", "KR"):
text = normalize_korean(text)
if language == "AUTO": if language == "AUTO":
# 영어 구간엔 사용자가 고른 억양, 한국어 구간엔 KR 음성 # 영어 구간엔 사용자가 고른 억양, 한국어 구간엔 KR 음성
en_speaker = speaker if (speaker and speaker.startswith("EN")) else "EN-US" en_speaker = speaker if (speaker and speaker.startswith("EN")) else "EN-US"