본문 바로가기
IT/인공지능

[TTS] 외래어와 수사를 고려한 한국어 텍스트 발음 변환

by 드인 2024. 3. 24.

[TTS] 외래어와 수사를 고려한 한국어 텍스트 발음 변환


문제 상황

Open AI의 TTS API로 한국어 음성을 생성하는데,

발음이 상당히 영어와 비슷하고 수사(사물의 수량이나 순서를 가리키는 품사)에 있어서 변환이 부정확했습니다.

  • 1개
    • TTS API: 일개
    • 실제: 한개
  • 26살
    • TTS API: 이십육살
    • 실제: 스물여섯살

 

솔루션 제안

  • 수사 → 단위 의존 명사에 따른 변환 로직
  • 영어와 비슷한 발음 → 한국어 발음외래어 전처리

 

한국어 발음 변환

한국어 발음을 변환하는 KoG2Padvanced 모델이 개발되어 있었습니다.

음운론적 규칙을 반영해서 발음 변환에 있어서 가장 적합하다고 생각됩니다.

하지만, 아라비아 숫자 형태에 대한 수사를 다룬 부분이 없어서 추가로 가공하기로 했습니다.

 

단위 의존 명사에 따른 변환

단위 의존 명사(명,가지,개, 달러, 원 등)에 따라 적절한 수사 발음을 변환했습니다. 

 

외래어 전처리

영어와 비슷한 발음이 유독 영어로 표기된 단어에서 자주 나타는 것을 발견하고, 외래어 변환을 통해 한국어 발음과 유사하게 변환했습니다.

 

솔루션 구현

from G2P.KoG2Padvanced import KoG2Padvanced
from konlpy.tag import Kkma
from TextUtil.digit2text import digit2txt, NNGdigit2txt, CSign2txt
from TextUtil.eng2text import eng2txt
import re

def process_txt(text, engtrans = False):
    kkma = Kkma()

    result = ""
    pattern = re.compile(r'([가-힣]+)|([a-zA-Z.]+)|(\d[\d,.]*)|(\$|€|£|¥|₩)|(\s+)')

    matches = pattern.finditer(text)
    for match in matches:
        if match.group(1):  # Korean part
            result += KoG2Padvanced(match.group(1))
        elif match.group(2):  # English or special characters part
            if engtrans == True:
                if match.group(2)[1:].islower():
                    result += eng2txt(match.group(2))
                else:
                    result += match.group(2)
            else:
                result += match.group(2)
        elif match.group(3):  # Number part
            end_index = match.end(3)
            # NNG Case
            next_word = kkma.pos(text[end_index:])[0]
            if next_word[1] == "NNG" and next_word[0] not in ['달러', '유료', '파운드', '엔', '원']:
                result += KoG2Padvanced(NNGdigit2txt(match.group(3).replace(',', '')))
            else:
                result += KoG2Padvanced(digit2txt(match.group(3).replace(',', '')))
        elif match.group(4):  # Currency symbol part
            result += CSign2txt(match.group(4))
        elif match.group(5):  # Space part
            result += match.group(5)
    return result

 

konlpy 라이브러리단위 명사를 파악하고, 해당 명사를 대상으로 수사 변환을 수행했습니다.

 

 

단위 의존 명사에 따른 변환

#-*- coding:utf-8 -*-

# 만 단위 자릿수
tenThousandPos = 4
# 억 단위 자릿수
hundredMillionPos = 9
txtDigit = ['', '십', '백', '천', '만', '억']
txtNumber = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구']
txtPoint = '쩜 '

def digit2txt(strNum):
    resultStr = ''
    digitCount = 0
    # print(strNum)
    #자릿수 카운트
    for ch in strNum:
        # ',' 무시
        if ch == ',':
            continue
        #소숫점 까지
        elif ch == '.':
            break
        digitCount = digitCount + 1


    digitCount = digitCount-1
    index = 0

    while True:
        notShowDigit = False
        ch = strNum[index]
        #print(str(index) + ' ' + ch + ' ' +str(digitCount))
        # ',' 무시
        if ch == ',':
            index = index + 1
            if index >= len(strNum):
                break;
            continue

        if ch == '.':
            # [수정] 0.13 처럼 1이하의 값에 대한 처리 추가
            if strNum[index - 1] == '0' and not resultStr:
                resultStr = '영'
            resultStr += txtPoint
        else:
            # 자릿수가 2자리이고 1이면 '일'은 표시 안함.
            # 단 '만' '억'에서는 표시 함
            # [수정] digitCount >= 1으로 설정하여 '십' 단위에서도 표시
            if(digitCount >= 1) and (digitCount != tenThousandPos) and  (digitCount != hundredMillionPos) and int(ch) == 1:
                resultStr = resultStr + ''
            elif int(ch) == 0:
                resultStr = resultStr + ''
                # 단 '만' '억'에서는 표시 함
                if (digitCount != tenThousandPos) and  (digitCount != hundredMillionPos):
                    notShowDigit = True
            else:
                resultStr = resultStr + txtNumber[int(ch)]


        # 1억 이상
        if digitCount > hundredMillionPos:
            if not notShowDigit:
                resultStr = resultStr + txtDigit[digitCount-hundredMillionPos]
        # 1만 이상
        elif digitCount > tenThousandPos:
            if not notShowDigit:
                resultStr = resultStr + txtDigit[digitCount-tenThousandPos]
        else:
            if not notShowDigit:
                resultStr = resultStr + txtDigit[digitCount]

        if digitCount <= 0:
            digitCount = 0
        else:
            digitCount = digitCount - 1
        index = index + 1
        if index >= len(strNum):
            break;
    return resultStr


NATIVE_MAP_ONES = {
    # "하나": 1, "둘": 2, "셋": 3, "넷": 4, "다섯": 5,
    "한": 1, "두": 2, "세": 3, "넷": 4, "다섯": 5,
    "여섯": 6, "일곱": 7, "여덟": 8, "아홉": 9,
    # "한": 1, "두": 2, "세": 3, "석": 3, "서": 3, "네": 4, "넉": 4, "너": 4,
    # "닷": 5, "엿": 6
}

MAP_TENS = {
    "열": 10, "스물": 20, "서른": 30, "마흔": 40, "쉰": 50,
    "예순": 60, "일흔": 70, "여든": 80, "아흔": 90
}


def NNGdigit2txt(number):
    # 이 함수는 주어진 숫자를 한국어 발음으로 변환합니다.
    # 예: 25 -> "스물다섯", 91 -> "아흔하나"
    # 여기서는 십 단위와 기본 숫자의 조합만을 고려합니다.
    korean_number = ""
    number = int(number)

    if  number >= 100:
        korean_number = digit2txt(str(number))
    elif number < 10:
        for key, value in NATIVE_MAP_ONES.items():
            if value == number:
                return key
    else:
        tens = number // 10
        ones = number % 10

        for key, value in MAP_TENS.items():
            if value == tens * 10:
                korean_number += key

        for key, value in NATIVE_MAP_ONES.items():
            if value == ones:
                korean_number += key

    return korean_number

def CSign2txt(csign):
    currency_symbols = {
        "$": "달러",
        "€": "유로",
        "£": "파운드",
        "¥": "엔",
        "₩": "원"
    }

    return currency_symbols.get(csign, "")

 

.(점)을 .(Dot)으로 읽는 경향이 있어서 소수점이 포함된 경우를 추가로 전처리했습니다.

 

외래어 전처리

from hangulize import hangulize

def eng2txt(text):
    # 영어와 비슷한 cym 사용
    return hangulize(text, 'cym')

 

hangulize 라이브러리를 사용하여 외래어 전처리 작업을 수행했습니다.

 

솔루션 결과물

활용 예시

>>> from TextUtil.process_text import process_txt
>>> InputText = "이 회사의 CEO는 project를 진행하면서 4개월 만에 934,500,000$의 수익을 올렸습니다."

>>> print(process_txt(InputText, True)) # 텍스트, 외래어 변환 여부

이 회사에 CEO는 프로젝트를 지냉아면서 사개월 마네 구만삼천사배고심만달러의 수이글 올렫씀니다.

 

 

https://github.com/0525hhgus/ko-text2pronun/

 

GitHub - 0525hhgus/ko-text2pronun: 외래어&수사 변환이 추가된 한국어 텍스트 발음 변환 도구

외래어&수사 변환이 추가된 한국어 텍스트 발음 변환 도구. Contribute to 0525hhgus/ko-text2pronun development by creating an account on GitHub.

github.com

 

https://huggingface.co/spaces/Hyeonseo/Text-to-Speech-Korean_Digit2Text

 

Text-to-Speech-Korean Digit2Text - a Hugging Face Space by Hyeonseo

Spaces

huggingface.co