
50음절을 전부 만들어서 이어 붙였더니, ( ´∀` )가 되어버린 이야기
요약
음절별 음성 파일을 이어 붙이는 방식과 AI 기반 TTS의 차이점을 스펙트로그램을 통해 분석합니다. 소리의 물리적 특성과 성대 및 구강 구조의 역할을 바탕으로 자연스러운 음성 합성의 원리를 설명합니다.
핵심 포인트
- 단순 음절 결합 방식의 한계와 끊김 현상 분석
- 스펙트로그램을 이용한 주파수와 시간의 시각화
- 성대의 떨림(재료)과 구강 구조(가공)의 메커니즘 이해
- AI가 음성 합성의 불연속성을 해결하는 방식 소개
음성①: 직접 만든 TTS로 "안녕하세요"
어디선가 들어본 듯한 느낌, 들지 않나요?
∧_∧
( ´∀`) < こんにちは (안녕하세요)
( )
...
그렇습니다, 모나(Mona)입니다.
일단 본인(?)은 "안녕하세요"라고 말하고 있는 셈입니다. 말해주면 알 정도일까요.
이 소리는 녹음을 전혀 사용하지 않았습니다. '아이우에오'부터 '응(ん)'까지 104음절을 하나씩 코드로 만들고, 사전에서 골라 이어 붙인 것입니다.
반면, 이쪽은 piper-plus라는 음성 합성 엔진으로 만든 소리입니다. 똑같은 "안녕하세요"입니다.
음성②: piper-plus로 만든 것
똑같은 "문자에서 소리로"인데, 이 차이입니다.
그 사이에는 무엇이 있는 걸까요?
- TTS라는 말은 들어봤지만, 내부에서 어떤 일이 일어나는지는 설명할 수 없다
- AI 음성이 자연스럽게 들리는 이유를 어렴풋이만 말할 수 있다
- 음성 처리는 수식이 많아 보여서 피해 왔다
- 라이브러리를 호출하면 작동한다는 것은 알지만, 그 한 줄의 내용이 궁금하다
Python의 기본 문법을 안다면 읽을 수 있습니다. 수학과 신호 처리(Signal Processing) 지식은 사용하지 않습니다.
도입부에서 제가 작성한 모나 목소리가 왜 그렇게 뚝뚝 끊겨 들리는지, 그 원인을 밝혀냅니다. 그 후, 그럼에도 남아있는 차이를 piper-plus가 어떻게 메우고 있는지 살펴봅니다.
추적할 질문은 3가지입니다.
① 소리를 나열하는 것만으로는 왜 목소리가 되지 않는가
② 무엇을 고쳐야 목소리에 가까워지는가
③ 그럼에도 남는 차이는 무엇인가. AI는 그 부분을 어떻게 해결하고 있는가
음성 합성의 사양을 망라하는 기사는 아닙니다. 우선적으로 파악해 두어야 할 메커니즘에 집중합니다.
TTS(Text-to-Speech)는 문자를 전달하면 음성이 돌아오는 메커니즘을 말합니다. 일본어로는 음성 합성이라고 부릅니다.
도입부의 모나도 문자를 넣으면 음성이 나왔으므로, 이것도 훌륭한 TTS입니다. 모나이긴 하지만요.
문자에서 음성을 만들라고 한다면, 가장 먼저 무엇이 떠오를까요?
가장 솔직한 방법은 다음과 같을 것입니다.
아이우에오부터 「ん」까지, 한 음절씩 음성 파일을 준비해 둔다
↓
「こんにちは(안녕하세요)」가 들어오면, こ・ん・に・ち・わ 의 파일을 선택한다
...
실제로 했던 것이 도입부의 소리입니다. 사용한 것은 104음절입니다. "50음(50音)"이라고 말하고 싶지만, 탁음과 반탁음, 그리고 「きゃ(캬)」와 같은 요음(拗音)을 더하니 104개가 되었습니다.
「こんにちは」라면 사전에서 5개가 선택됩니다.
こんにちは → こ / ん / に / ち / わ
끝부분이 「は」가 아니라 「わ」인 이유는, 조사 「は」를 「わ」로 읽는 대응을 사전에 넣어두었기 때문입니다. 나머지는 이 5개의 파일을 순서대로 잇기만 하면 됩니다. 길이는 1.54초가 되었습니다.
문자는 음성이 되었습니다. 다만, 모나입니다.
귀로 듣기에 "뚝뚝 끊긴다"는 것은 알 수 있습니다. 하지만 무엇이 끊기는 것인지는 귀만으로는 알 수 없습니다.
그래서 소리를 눈으로 봅니다. 사용하는 것은 **스펙트로그램 (Spectrogram)**이라는 도표입니다. 세로축은 주파수(Frequency), 가로축은 시간(Time), 밝은 부분이 해당 주파수의 강한 부분. 그뿐인 도표입니다.
읽는 법을 익히기 위해, 먼저 "아이우에오"를 찍어보겠습니다.
음성③: 아이우에오
밝은 띠가 아래에 한 줄, 그 위에도 한 줄 있습니다. 모음이 바뀔 때마다 이 두 줄이 상하로 움직이는 것이 보이시나요?
왜 두 줄의 띠가 움직이는 걸까요? 사람이 소리를 내는 메커니즘과 관계가 있습니다.
소리를 내기 위해서는 재료와 가공 두 가지가 필요합니다.
재료: 목구멍 깊은 곳의 성대가 숨에 의해 떨리며 "부우" 하는 소리를 만든다
가공: 그 소리가 입안을 통과할 때, 입과 목의 모양이 특정 높이만을 강화한다
가공으로 강화된 위치가 스펙트로그램의 밝은 띠입니다. 띠는 낮은 쪽부터 순서대로 F1, F2……라고 번호가 붙습니다. 여러 개가 있지만, 모음을 결정하는 것은 아래의 두 줄, F1과 F2입니다. 이후에는 이 두 가지만 추적하겠습니다.
이 부분이 핵심인데, 재료 쪽은 모음이 바뀌어도 동일합니다. 「아」라고 말할 때나 「이」라고 말할 때나 성대는 같은 소리를 내고 있습니다. 변하는 것은 가공뿐입니다. 따라서 모음의 차이는 포먼트 (Formant) 위치의 차이가 됩니다.
그 F1과 F2가 무엇인지는 입을 움직여보면 알 수 있습니다. 「아」와 「이」를 소리 내지 말고 입 모양만으로 말해 보세요.
「아」는 입을 크게 벌리고 혀는 뒤에 있습니다. 「이」는 입이 좁아지며 혀가 앞으로 나옵니다.
F1 = 입을 얼마나 벌리고 있는가 (크게 벌릴수록 높음)
F2 = 혀를 얼마나 앞으로 내밀고 있는가 (앞으로 낼수록 높음)
「あ」는 F1이 높고 F2가 낮습니다. 「い」는 그 반대입니다. F1과 F2는 입의 벌림 정도와 혀의 위치를 숫자로 나타낸 것이라고 생각하시면 됩니다.
귀로 확인하는 것이 더 빠르므로, 재료가 되는 소리부터 차례대로 더해 나가겠습니다. 우선 가공하지 않은, 성대의 소리 그 자체입니다.
음성④: 재료가 되는 소리 (성대의 소리만, 가공 없음)
부저와 같은 소리입니다. 이 시점에서는 아직 모음이 아닙니다.
여기에 가공을 하나, F1만을 더합니다.
음성⑤: F1만을 더한 소리
조금 먹먹해졌습니다. 아직 모음은 아닙니다.
여기에 F2 이상의 대역도 더합니다.
음성⑥: 포먼트 (Formant)를 전부 더한 소리
「あ」가 되었습니다. 재료가 되는 소리는 음성④ 때와 비교해 아무것도 바꾸지 않았습니다. 강화할 위치를 더했을 뿐입니다.
그렇다면, 그 위치의 수치를 바꾸면 어떻게 될까요?
# 「あ」 입을 크게 벌리고, 혀는 뒤쪽에
F1 = 800
F2 = 1200
...
움직인 것은 이 두 가지뿐입니다. 위쪽의 대역은 그대로 두었습니다.
음성⑦: 수치만 바꾼 소리
「い」가 되었습니다.
모음의 차이는 F1과 F2의 위치로 거의 결정됩니다.
| 모음 | F1 [Hz] | F2 [Hz] |
|---|---|---|
| あ | 800 | 1200 |
| ... |
한꺼번에 전환하는 것이 아니라, 2.5초에 걸쳐 연속적으로 움직이면 어떻게 될까요? 이 부분이 나중의 이야기에 영향을 미칩니다.
음성⑧: 「あ」에서 「い」로
그림에서는 두 개의 선이 왼쪽에서 오른쪽으로 멀어집니다. 아래쪽 선은 800Hz에서 300Hz로 내려가고, 위쪽 선은 1200Hz에서 2300Hz로 올라갑니다.
소리 또한 「あ」에서 「い」로 경계 없이 변했을 것입니다.
즉 스펙트로그램 (Spectrogram)은 입의 모양이 시간에 따라 어떻게 움직였는지를 기록한 것입니다. 이를 통해 원인을 찾을 수 있습니다.
서두의 소리와, 이후에 수정할 소리를 나란히 놓겠습니다.
위쪽은 104음절을 이어 붙인 것이고, 아래쪽은 수정한 것입니다. 똑같은 「こんにちは」입니다.
위쪽을 보세요. 5개의 덩어리가 뚝뚝 끊겨 나열되어 있습니다. 그리고 덩어리 안에서는 대역이 거의 가로로 평행합니다.
아래쪽은 끊김이 없습니다. 대역이 대각선으로 이어져 있습니다.
그림과 소리를 나란히 놓고 확인할 수 있도록 둘 다 놓아두겠습니다.
음성① (재게시): 104음절을 이어 붙인 것 (위쪽 그림)
음성⑨: 한 호흡에 합성한 것 (아래쪽 그림)
아래쪽 그림의 0.3초 부근. 선이 대각선으로 올라가고 있습니다.
방금 「あ→い」에서 보았던 것과 같은 패턴입니다. 위쪽 대역이 1000Hz에서 2300Hz로 이동하고 있습니다.
이것은 「ん」에서 「に」로 넘어갈 때, 입의 모양이 움직이고 있는 도중입니다.
사람이 「んに」라고 말할 때, 「ん」의 모양에서 멈췄다가 다음 순간에 「に」의 모양으로 순간 이동하지 않습니다. 혀와 입술은 연속적으로 움직입니다. 그 이동의 궤적이 그대로 소리에 남습니다.
이를 **전이 (Transition/渡り)**라고 부릅니다.
여기가 정답입니다.
「こ」를 단독으로 만들어도, 「ん」을 단독으로 만들어도, 그 파일 안에는 전이가 들어있지 않습니다. 전이는 두 소리의 경계에만 있기 때문입니다.
즉 104음절을 하나씩 만든 시점에서, 전이는 단 하나도 만들어지지 않았던 것입니다. 만들어지지 않은 것은 나중에 이어 붙여도 나타나지 않습니다.
게다가 사람의 귀는 이 전이를 단서로 자음을 구별해냅니다. ba
da
ga
의 차이는 모음으로 향하는 대역의 움직임 차이로 거의 결정됩니다.
전이를 놓치면 알아듣기 어려워진다. 모나(Mona)가 되는 것은 당연한 결과였습니다.
나온 결과는 세 가지입니다.
**포먼트 (Formant)**는 입과 목의 모양에 따라 강화되는 주파수를 말합니다. 스펙트로그램에서는 밝은 대역으로 보입니다. 낮은 쪽부터 F1, F2라고 부릅니다.
모음의 차이는 F1과 F2가 어디에 있느냐로 거의 결정됩니다.
전이는 어떤 소리에서 다음 소리로 넘어갈 때 입이 움직인 흔적입니다. 소리 내부가 아니라, 소리와 소리 사이에만 존재합니다.
원인을 알았으니 수정하겠습니다.
한 글자씩 만드는 것을 그만두고, 문장 전체를 한 번에 전달합니다.
# 수정 전: 한 글자씩 합성하여 나중에 이어 붙임
for kana in ["こ", "ん", "に", "ち", "わ"]:
save_wav(synthesize(kana)) # → 이어 붙임 → 모나
...
동일한 함수입니다. 새로운 처리는 한 줄도 쓰지 않았습니다. 전달하는 단위만 바꿨을 뿐입니다.
포먼트를 만들고 있는 부분은 이런 형태를 하고 있습니다. 내용은 읽지 않으셔도 됩니다. 인수의 타입(Type)만 확인해 주세요.
def _resonator(x: np.ndarray, freq: np.ndarray, bw: np.ndarray) -> np.ndarray:
...
freq는 단일 값이 아니라 **배열 (Array)**입니다. 포먼트(Formant)의 위치를 1샘플마다 움직일 수 있도록 되어 있습니다. 조금 전 「아→이」를 연속적으로 변화시켰을 때 사용한 것이 바로 이 성질입니다.
문장 전체를 전달하면, 내부에서는 음소(Phoneme)의 나열로부터 하나의 연속된 궤적이 구성됩니다.
# 음소의 중심에 포먼트의 목표값을 두고, 사이는 보간(Interpolation)으로 채운다
hold = 0.34 if phone.kind == "vowel" else 0.42
for at in (t + duration * hold, t + duration * (1.0 - hold)):
...
각 음소가 배치하는 것은 "이 시점에는 이 형태"라는 **목표 (Target)**뿐입니다. 목표와 목표 사이는 보간을 통해 자동으로 채워집니다.
이 보간이 그대로 연결(Transition)이 됩니다.
한 음소씩 합성했을 때는 각 파일이 자신만의 목표만을 가지고 있었습니다. 사이를 채워줄 상대가 없었기 때문에 대역(Band)이 평행하게 되어 있었던 것입니다.
문장 전체를 전달할 때, 내부에서는 「곤니치와(안녕하세요)」가 다음과 같이 분해됩니다.
こんにちわ → k o N n i C i w a
9개가 있습니다. 이것이 「곤니치와」를 소리의 입자 단위까지 나눈 것입니다. C는 「치」의 자음, N은 「ん」을 나타냅니다.
「치」가 한 글자인데 C와 i 두 개로 나누어져 있다는 점에 주목해 주세요. 글자가 아니라, 실제로 내고 있는 소리로 나누고 있습니다. 이 입자 하나하나를 **음소 (Phoneme)**라고 부릅니다.
조금 전 그림과 함께 들었던 두 가지를 다시 나열합니다.
음성 ① (재게시): 이어 붙인 것
음성 ⑨ (재게시): 한 호흡에 합성한 것
길이도 변했습니다. 1.54초에서 1.05초로, 32% 짧아졌습니다.
이어 붙인 버전에서는 각 소리가 자신만의 상승(Attack)과 감쇠(Decay)를 가지고 있었습니다. 5음분량의 상승과 감쇠가 전부 남기 때문에 늘어지게 됩니다. 한 호흡에 만들면 그 중복이 사라집니다.
또 다른 한 쌍.
음성 ⑩: 아리가토 (이어 붙인 것)
음성 ⑪: 아리가토 (한 호흡)
TTS가 만들고 있는 것은 소리의 나열이 아니었습니다. 시간에 따라 변해가는 입의 모양입니다.
음소는 소재가 아니라, 통과점이었다고 바꿔 말해도 좋을 것 같습니다.
이 사고방식은 포먼트 합성 (Formant Synthesis)이라 불리며, 1980년대까지 TTS가 도달했던 지점입니다. 그 시대의 말하는 장난감 목소리가 바로 이 소리입니다.
말로는 알아들을 수 있게 되었습니다. 하지만 서두의 piper-plus에는 아직 멀었습니다.
무엇이 부족한지 나열해 보겠습니다.
| 관점 | 직접 만든 것 | piper-plus |
|---|---|---|
| 읽을 수 있는 글자 | 가나(Kana)뿐. 한자는 읽지 못함 | 한자를 포함하여 읽음 |
| ... | 반영됨 | |
| 목소리의 개성 | 없음. 누구의 목소리도 아님 | 특정 화자의 목소리가 됨 |
억양을 예로 들어 보겠습니다. 일본어에는 **피치 액센트 (Pitch Accent)**가 있어, 같은 가나라도 높낮이에 따라 의미가 변합니다. 「하시(다리)」와 「하시(젓가락)」, 「아메(비)」와 「아메(사탕)」. 이번 구현은 입력과 무관한 고정 커브를 사용하고 있기 때문에 둘 다 같은 소리가 됩니다.
5가지 공통점은, 모두 「올바른 수치를 어떻게 결정할 것인가」의 문제라는 점입니다.
서두의 음성 ②를 만든 엔진입니다. 학습된 AI 모델로 일본어를 말하게 할 수 있으며, 심지어 가진 PC에서 동작합니다. Piper라는 음성 합성 엔진에서 파생된 것입니다.
하고 있는 일을 크게 나누면 2단계가 됩니다.
문장
↓
【전반부】 글자를 음소로 변환한다 ← 언어별 분석기가 담당
...
실제로 돌려보겠습니다. 수중에 있는 환경에서 「곤니치와」를 만들어, 서두에서 들었던 소리와 같은 것이 나오는 것을 확인합니다.
pip install piper-plus
python -m piper --model ja_JP-tsukuyomi-chan-medium \
--length-scale 1.2 --noise-scale 0.5 \
...
지정하고 있는 것은 3가지입니다.
--model: 사용할 목소리.ja_JP-tsukuyomi-chan-medium이 일본어 모델입니다.--length-scale: 발화의 길이. 크게 하면 느려집니다.--noise-scale: 생성의 변동성 (Variability)
최초 1회만 모델이 자동으로 다운로드됩니다. .onnx라는 확장자를 가진 파일로, 학습된 모델의 저장 형식입니다. PyTorch 등 무엇으로 학습했는지와 관계없이 실행할 수 있는 공통 포맷으로 되어 있습니다.
나온 konnichiwa.wav가 도입부의 소리입니다. 여기서부터 내부를 살펴볼 것이므로, 다시 한번 놓아두겠습니다.
음성② (재게시): piper-plus로 만든 것
나오는 WAV는 16bit PCM 모노럴로, 직접 만든 것과 같은 형식입니다. 모델의 내용이 달라도 출력(exit)은 변하지 않습니다.
전반부는 문자를 음소(phoneme)로 변환하는 부분입니다. ②장에서 "こんにちわ(콘니치와)"를 k o N n i C i w a로 나누었지만, 그것과 같은 작업을 한다고 생각하시면 됩니다.
일본어의 경우, 이 부분이 의외로 무거운 처리가 됩니다. 한자의 읽기를 결정하고, 조사 「は(하)」를 「わ(와)」로 바꿔 읽으며, 악센트 위치를 판단합니다. 직접 만든 쪽은 수기 사전으로 최소한만 대응하고 있었습니다.
piper-plus는 여기에 OpenJTalk를 사용하고 있습니다. 설치했을 때의 의존성(dependency)에 그대로 나타납니다.
Collecting pyopenjtalk-plus>=0.4.1.post8 (from piper-plus)
OpenJTalk는 일본어 전용 언어 분석기로, 한자의 읽기도 악센트도 다룰 수 있습니다.
즉, piper-plus가 독자적으로 하고 있는 것은 전반부인 "문자를 음소로 변환하는 부분"입니다. 후반부인 "음소로부터 파형을 만드는 부분"은 파생 원본인 Piper와 같은 것을 사용합니다.
전반부를 교체하는 것만으로 대응할 수 있는 언어가 늘어난다. 전반부와 후반부가 나누어져 있는 이점이 여기에 나타나 있습니다.
직접 만든 쪽은 「あ(아)」를 130ms로 못 박아 두었습니다. 학습 측은 다음과 같이 합니다.
먼저, 음성 데이터와 음소의 나열을 대조하여, 파형의 어느 구간이 어느 음소인지 대응시킵니다. 이를 **얼라이먼트 (Alignment)**라고 부릅니다.
음소열 k o N n i
파형 ├──┤├────┤├────┤├─┤├───┤
길이 55ms 120ms 95ms 40ms 110ms
대응이 완료되면 각 음소가 실제로 몇 밀리초(ms)였는지 추출할 수 있습니다. 수만 개의 문장을 모으면 "이 음소가, 이 전후 관계에서, 문장의 이 위치에 있을 때 몇 밀리초인가"라는 데이터의 산이 만들어집니다. 그다음은 그것을 예측하는 함수를 학습시키기만 하면 됩니다.
동일한 얼라이먼트로부터 목소리의 높낮이도 추출할 수 있으므로, 억양도 같은 방식으로 학습됩니다. 「はし(하시)」가 「橋(다리)」라면 낮음에서 높음으로, 「箸(젓가락)」라면 높음에서 낮음으로. 그 차이를 문맥과 함께 학습합니다.
참고로, 구분 레이블은 학습 데이터에 붙어 있지 않습니다. 사람이 수만 개의 문장을 일일이 구분하는 것은 무리가 있으므로, 모델 스스로 찾게 합니다. 단서는 "음소는 순서대로 발음된다"는 것뿐입니다. 순서가 고정되어 있다면, 나머지는 어디서 구분할지를 효율적으로 찾을 수 있습니다.
이 부분이 가장 큰 차이점입니다.
직접 만든 쪽이 했던 것은 성도(vocal tract)를 수식으로 모델링하는 것이었습니다. "목소리란 성대의 소리가 3개의 포먼트(formant) 공명을 통과한 것이다"라고 정하고, 그 구조를 코드로 구현했습니다.
voiced = _resonator(source, f1, bw1) # 제1 포먼트
voiced = _resonator(voiced, f2, ...) # 제2 포먼트
voiced = _resonator(voiced, f3, ...) # 제3 포먼트
학습된 모델은 이 구조를 만들지 않습니다.
성도가 몇 개의 공명을 갖는지도, 성대파가 어떤 형태인지도 적혀 있지 않습니다. 대신 대량의 파라미터(parameter)를 가진 함수를 준비하여, 학습 데이터의 파형을 재현할 수 있도록 파라미터를 조정합니다.
직접 만든 쪽: 인간이 "목소리는 이런 구조다"라고 정하고, 그 식에 수치를 넣음
학습된 모델: 구조를 정하지 않고, 데이터의 파형을 재현할 수 있도록 파라미터를 조정함
공진기 3개로 나타낸 성도는 사람 목소리의 "대략적인 형태"일 뿐입니다. 실제 목소리에는 숨이 섞이는 방식, 성대가 닫히는 습관, 코로 빠져나가는 양 같은 특징이 무수히 많습니다. 전부 써 내려가는 것은 불가능합니다. 써 내려가는 것을 그만두고, 데이터로부터 베껴낸 것이 학습된 모델입니다.
piper-plus가 사용하는 모델 설계에는 VITS라는 이름이 붙어 있으며, 추론(inference) 시에는 다음과 같이 흐릅니다.
음소의 나열
↓ Text Encoder 각 음소를 특징 벡터(feature vector)로 만듦 (전후 음소의 정보가 섞임)
↓ Duration Predictor 각 음소가 몇 프레임 분량인지 결정하고, 그 수만큼 늘림
...
마지막 Decoder가, 직접 만든 공진기(Resonator) 3개가 수행하던 역할에 해당합니다.
한 가지 더. 같은 문장에 대응하는 음성은 하나로 결정되지 않습니다. 천천히 말한 "안녕하세요"도, 밝게 말한 "안녕하세요"도 학습 데이터에는 모두 들어있습니다. 정해진 대응 관계를 배우려고 하면 전부의 평균값이 되어, 어느 것도 아닌 흐릿한 소리가 나오고 맙니다. 그래서 VITS는 음성을 분포(Distribution)로 취급하며, 생성 시 그중 하나를 선택합니다. 같은 문장을 두 번 생성해도 완전히 똑같은 파형(Waveform)이 되지는 않습니다.
piper-plus의 내부를 전반부와 후반부로 나누어 살펴보았습니다.
전반부에서 음소화(Phonemization)를 담당했던 것은 OpenJTalk였습니다. 문자를 음소의 나열로 바꾸는 부분으로, 직접 만든 방식이라면 수기로 작성한 읽기 사전(Reading Dictionary)에 해당합니다.
후반부를 담당했던 것은 학습된 모델(Pre-trained Model)입니다. 각 음소의 길이를 결정하고, 억양을 붙이고, 파형을 출력합니다. 이 세 가지는 모두 실제 사람의 발화 데이터에서 학습한 값을 사용하고 있었습니다.
직접 만든 방식은 그 값을 전부 코드의 표에 적어두었습니다. 큰 차이점은 그 값을 누가 결정하는가뿐입니다.
추적해 온 세 가지 질문에 한 줄씩 답하겠습니다.
① 소리를 나열하는 것만으로는 왜 목소리가 되지 않는가
연결(渡り, Transition)이 없기 때문입니다. 연결은 소리와 소리 사이에만 존재하며, 음소를 하나씩 만든 시점에서는 생겨나지 않습니다.
② 무엇을 고쳐야 목소리에 가까워지는가
한 글자씩 만드는 것을 그만두고, 문장 전체를 한 번에 전달하는 것입니다. 새로운 코드는 한 줄도 필요하지 않습니다.
③ 그럼에도 남는 차이는 무엇인가. AI는 그 부분을 어떻게 풀고 있는가
"올바른 수치를 누가 결정하는가"의 차이입니다. 사람이 쓰는 것을 그만두고, 발화 데이터로부터 학습하게 만든 것이 지금의 TTS였습니다.
우리가 평소 말할 때, 단어와 단어 사이의 일은 의식하지 않습니다. 입이 어떻게 움직였는지, 몇 밀리초(ms)를 늘렸는지, 어디서 목소리를 높였는지. 아무도 세지 않고, 기억하지도 않습니다.
하지만 기계가 말하게 하려면, 그것을 전부 숫자로 만들 필요가 있습니다. 이 기사에서 수행한 것은 바로 그 번역 작업이었습니다.
104음 분량의 고정값까지는 손으로 쓸 수 있었습니다. 쓸 수 없는 것은 조건에 따라 매번 변하는 부분입니다. 그 부분을 사람이 쓰는 것을 그만둔 것이 지금 매일 듣고 있는 목소리입니다.
도입부의 모나(Monar)와 그 자연스러운 목소리. 그 사이에 있었던 것은 숫자를 누가 결정하는가의 차이였습니다.
이번 기사에서는 평소 무심코 사용하고 있는 음성 합성의 구조를 직접 만들어보며 하나씩 확인해 보았습니다.
Sapeet에서는 이와 같이 AI나 알고리즘의 구조를 깊이 파고들어, 실제로 직접 움직이며 프로덕트나 업무로 연결해 나가는 것을 소중히 여깁니다.
그런 Sapeet의 기술과 개발 분위기를 조금 더 알 수 있는 기회로서, 교류 이벤트 「Open Sapeet!」를 개최합니다.
2026년 9월 11일(금) 19:30부터, Sapeet의 엔지니어를 비롯한 사원들이 등단하여 AI와 프로덕트 개발, 앞으로의 일하는 방식에 대해 이야기합니다. 이 기사에서 엿보았던 것과 같은 "AI 내부에서 어떤 일이 일어나고 있는가"에 가까운 이야기도 나올지 모릅니다.
토크 이후에는 가벼운 음식과 음료를 곁들이며 편하게 교류할 수 있는 시간도 마련되어 있습니다.
스타트업의 개발 현장을 엿보고 싶은 분, Sapeet의 분위기를 알고 싶은 분 모두 부담 없이 놀러 오세요.
이벤트 상세 정보 및 신청은 이쪽으로
∧_∧
( ´∀`) < 느긋하게 기다리고 있을게
( )
...
--
monar-tts — 이 기사에서 만든 TTS의 코드와 104음.
pip install numpy만으로 동작합니다.
piper-plus — 기사에서 사용한 엔진. MIT 라이선스. PyPI에서 pip install piper-plus로 설치 가능.
-
일본어 모델은
ja_JP-tsukuyomi-chan-medium을 사용했습니다. 음성 모델의 라이선스는 배포처의 기재 사항을 확인해 주세요. -
Piper (OHF-Voice/piper1-gpl) — piper-plus의 파생 원본. 본체는 GPL-3.0
-
OpenJTalk — piper-plus가 일본어 음소화에 사용하고 있는 분석기
-
VITS (Kim et al., 2021) — Monotonic Alignment Search, Flow, 확률적 길이 예측(Stochastic Duration Predictor)은 이 논문에 등장합니다.
-
HiFi-GAN (Kong et al., 2020) — VITS의 Decoder가 채택하고 있는 파형 생성 방식
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기