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
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user