TTS가 소설 전체를 읽다: 파이프라인에서 가장 신뢰할 수 있는 부분은 모델이었다
요약
크림타타르어 소설을 합성 음성으로 구현한 경험을 공유하며, TTS 시스템의 성공 여부는 모델 자체보다 텍스트 및 오디오 전처리 파이프라인에 달려있음을 강조합니다. 특히 문장 분리, 숨소리 제거 등 주변 과정에서 발생하는 버그가 대규모 콘텐츠에서 발견됨을 설명합니다.
핵심 포인트
- TTS 구현 시 핵심은 모델 성능보다 전처리 파이프라인 안정성이다.
- 텍스트를 문장 단위로 분리하고 오디오 데이터를 정제하는 작업이 중요하다.
- 소규모 테스트(14문장)로는 대용량 콘텐츠에서 발생할 수 있는 문제를 예측하기 어렵다.
이 시리즈의 첫 번째 게시물([https://dev.to/servin_osmanov/what-i-learned-building-speech-tech-for-a-language-with-zero-datasets-2gka])에서 언급했듯이, 합성된 목소리가 크림타타르어(Crimean Tatar)로 전체 소설 『Merdiven』("계단")을 읽은 내용을 하나의 표에 담았습니다. 샤밀 알라딘(Şamil Alâdin)의 작품으로, 총 16장 분량이며 오디오는 1시간 58분입니다. 두 번째 게시물([https://dev.to/servin_osmanov/fine-tuning-was-the-easy-part-what-shipping-asr-for-a-low-resource-language-taught-me-4331])에서는 이 표 뒤에 숨겨진 이야기를 약속했습니다. 여기 그 이야기입니다.
간단히 말해, 이와 유사한 것을 구축한다면: 모델은 제 역할을 해냈습니다. 문제는 모델 자체가 아니라 주변의 텍스트 및 오디오 준비 과정에 있었습니다. 저는 이 부분을 직접 작성했기 때문에 재학습 없이 수정할 수 있었습니다. 즉, 텍스트를 문장 단위로 분리하고, 짧은 문장들을 이어 붙이고, 숨소리를 잘라내고, 책을 파일에서 추출하는 작업, 그리고 결과물을 확인하는 과정이었습니다. 그리고 그 버그들 중 상당수는 데모에서는 절대로 나타날 수 없습니다. 그것들은 전체 챕터나 심지어 한 권의 책 분량이 되어야 비로소 모습을 드러냅니다.
컨텍스트 30초
크림타타르어에는 къ (a /q/)라는 발음이 있는데, 주변 언어에서 만든 기성 모델들은 이를 평범한 k 소리로 처리하여 'qara'("검은")가 _kara_로 들리게 합니다. 저는 카자흐어(Kazakh), 위구르어(Uyghur), 바시키르어(Bashkir)를 많이 들어본 다국어 기반 모델을 선택했고, 우리의 къ, гъ, нъ 발음을 이미 알고 있는 바시키르어 문자들인 ҡ, ғ, ң으로 표기했습니다. 그리하여 모델은 마치 바시키르어를 읽는 것처럼 우리 텍스트를 읽습니다. 이 모든 과정의 이야기는 첫 번째 게시물에 담겨 있습니다.
목소리는 별도의 입력값입니다. 제가 파인튜닝(fine-tuning)한 모델은 언어 자체를 학습할 뿐, 음색(timbre)을 학습하지 않습니다. 음색은 짧은 레퍼런스 클립, 즉 정확한 대본이 있는 몇 초 분량의 녹음에서 나옵니다. 『Merdiven』은 세빌(Sevil)의 목소리로 읽혔으며, 책의 모든 문장은 동일한 7초짜리 클립을 기반으로 조건화되었습니다. 이 점을 염두에 두세요. 나중에 다시 언급하겠습니다.
10분 동안의 음성 변화는 14개의 문장만으로는 알 수 없습니다
책을 시작하기 전에, 저는 모델 버전을 14개의 테스트 문장(probe sentences)에서 비교했습니다. 이 문장들에는 저희가 가진 가장 어려운 발음인 къ, гъ, нъ, дж를 의도적으로 채워 넣었습니다. 아주 정교한 회귀 세트입니다. 하지만 이것만으로는 장 끝에 음색이 변하는지, 모델이 대화, 이름, 숫자, 그리고 한 단어 답변을 어떻게 처리하는지, 또는 호흡은 어떤지를 전혀 알려주지 못합니다. 15초짜리 클립에서는 그 어떤 것도 들을 수 없습니다. 책 전체에서는 모든 것을 들을 수 있습니다.
그래서 다음 단계는 전체 장 하나였습니다. 소설 《Merdiven》의 첫 번째 장으로, 1,608 단어에 달하며 약 12분 분량입니다. 목적은 감탄하는 것이 아니었습니다. 문제가 되는 지점을 찾는 것이 목적이었습니다.
먼저 이 텍스트가 학습 자료에 포함되지 않았는지 확인했습니다. 거의 140만 개의 단어와 비교하여 연속된 여섯 단어가 일치하는 경우는 0건이었습니다. 그렇지 않다면 모델은 읽고 있는 것이 아니라 기억하고 있을 것입니다. 나중에 소설 전체도 같은 검사를 거쳤으며, 이 역시 일치하는 건수는 0건이었습니다.
왜 이 책이었을까요? 저는 Şamil Alâdin의 글쓰기를 좋아했고, 저희 언어로 기계가 읽는 첫 번째 책이 그의 작품이길 바랐기 때문입니다. 또한 모델에도 적합했습니다. 서사적 산문은 좋았지만, 목소리가 연기해야 하는 유머러스한 이야기나 아동 도서는 당시로는 역부족이었습니다.
첫 번째 수치만으로도 이미 교훈을 얻었습니다. 테스트 문장에서는 제가 저희 음성 인식기로 합성된 내용을 다시 들으면서 텍스트와 비교했을 때, 캐릭터의 약 14%가 잘못 나왔습니다. 하지만 장의 일반 산문 부분에서는 2% 미만이었습니다. 모델이 하룻밤 사이에 개선된 것은 아니었습니다. 테스트 세트는 설계상 적대적이며(hostile by design), 일반 텍스트는 그렇지 않습니다. 저는 더 이상 테스트 수치를 마치 모델이 실제로 읽는 방식을 설명하는 것처럼 인용하지 않습니다. 그것은 추정치가 아니라 경고 신호입니다(tripwire).
14개의 문장으로는 두 가지 수정 사항을 구별할 수 없었다
첫 번째 게시물에서 저는 소설이 'ниже' (러시아어로
입력된 소리(sound)를 철자하는 다섯 가지 방법을 비교했습니다. 14개의 테스트 문장에서는 두 가지 방법 모두 구별할 수 없을 정도로 좋았습니다. 둘 다 해당 단어를 고정시켰습니다. 그래서 저는 이 두 가지 방식으로 전체 장, 즉 195개 문장을 다시 렌더링해 보았습니다. 하나는 타타르 문자 җ를 사용했는데, нидже는 고정했지만 나머지 모든 것을 조용히 망가뜨렸고: 장 전체에 걸쳐 문자 오류율이 27% 증가했습니다. 단순한 дж를 사용한 방식은 해당 단어를 고정시키면서 아무것도 망가뜨리지 않았습니다.
14개 문장에서는 두 후보 모두 동등하게 좋다고 나왔지만, 한 장을 보여주자 그중 하나가 문제가 있다는 것이 드러났습니다. 실패했던 예시에 대해서만 확인된 수정 사항은 실제로 충분히 검증되지 않은 것입니다. 일반적인 텍스트로 충분히 돌려보면서 그것이 또 무엇에 영향을 주는지 봐야 합니다.
제1장에서 발생한 문제점들
제1장에 대한 결론은 저를 놀라게 했습니다. 목소리는 유지되었고: 음색은 처음부터 끝까지 거의 변하지 않았으며, 속도 또한 일정했습니다. 결함은 모델 자체에 있는 것이 아니라 텍스트와 오디오가 주변에서 어떻게 준비되었는지에 있었습니다. 제가 직접 작성한 부분이어서 좋은 소식이었습니다. 재훈련할 필요가 없었기 때문입니다.
짧은 문장을 붙이는 방식이 전체 구절을 떨어뜨림
모델은 최소 약 3초 길이의 클립으로 학습했기 때문에, 저는 두 단어로 된 한 줄이 혼자서 전달되어서는 안 된다고 생각하고 짧은 문장들을 이웃하는 문장에 붙여(glued) 처리했습니다. 그 논리는 완벽해 보였습니다. 하지만 저는 그것을 테스트하지 않았습니다. 그것이 실수였습니다.
붙여진 덩어리(glued chunks)로 모델을 돌렸더니, 모델이 전체 문장들을 떨어뜨리기 시작했습니다:
| 읽으라고 요청한 내용 | 출력된 내용 |
|---|---|
| Men yañılğanım. O, oca degil eken. | O, oca degil eken. |
| – Yoq. Siz yañlışasıñız. Meni bir daa iç bir yerde köralmaycaqsıñız. | Meni bir daa iç bir yerde köralmaycaqsıñız. |
오직 그때서야 저는 그 가정을 직접 테스트하고 아무런 접합(gluing) 없이 장을 렌더링했습니다. 짧은 문장이 정말로 더 나빴습니다. 대략 오류율이 두 배 반 정도 높았지만, 절대적인 수치로는 약 2% 포인트 차이였습니다. 오류는 약 70자까지 떨어지면서 계속 감소했고, 그 이후에는 거의 움직임이 없었습니다. 그리고 이 페널티가 문장 전체를 놓치는 접합의 비용보다 작다는 것이 밝혀졌습니다. 접합은 실패했습니다.

접합 없이 렌더링된 첫 번째 장. 이전에는 43자 미만의 모든 것이 이웃과 접합되었지만, 오류는 약 70자까지 떨어지면서 계속 감소했습니다.
모델이 '처리할 수 없다'고 하는 것에 대한 해결책을 만들기 전에, 그것을 실제로 얼마나 나쁘게 처리하는지 측정해 보세요. 여기서는 그 해결책 비용이 피하려 했던 문제보다 더 비쌌습니다.
호흡 자르기(breath trimmer)가 첫 단어를 먼저 먹어치우다
때로는 합성 과정이 인간 내레이터처럼 숨을 들이쉬는 소리와 함께 문장을 시작합니다. 저는 트리머를 작성했습니다. 첫 유성음(voiced sound) 이전의 모든 것이 호흡으로 간주되어 잘려나가는 방식입니다. 하지만 많은 첫 단어들은 무성 자음(voiceless consonant)으로 시작합니다. _Çañ qaqıldı_는 _Qaqıldı_로, _Tışta, yeşergen…_은 _yeşergen…_으로 변했습니다. 147개 클립 중 111개에서 잘림이 첫 단어 내부에서 발생했습니다. 이 장은 아무것도 자르지 않았을 때보다 제
첫 번째 검사에서는 문장의 89%가 들숨(inhale)으로 시작했다고 보고했습니다. 무시무시한 수치였습니다. 그러다 트리머(trimmer)가 자기 자신에 대해 보고하고 있다는 것이 밝혀졌습니다. 그것은 첫 유성음(voiced sound) 이전의 모든 것을 호흡으로 계산했는데, 여기에는 같은 무성 자음(voiceless consonants)도 포함되었습니다. 솔직한 측정 결과, 다섯 문장 중 한 문장에서만 시작 부분에 호흡이 발견되었습니다.
실제 호흡은 완전히 다른 곳에 있었습니다. 단어 사이, 정확히 트리머가 결코 보지 못하는 곳이었습니다. 195개 문장 중 76개 문장에 호흡이 있었습니다. 저는 타임스탬프를 하나도 움직이지 않으면서 그들을 배경 소음 수준으로 낮추는 방법을 배웠고, 76개 문장 중 68개가 이전과 정확하게 전사되었습니다.
무엇을 감지할지 정의하는 구성 요소는 항상 자기 자신과 일치합니다. 그리고 그것은 자신이 보는 곳에서만 무언가를 찾아냅니다.
속도와 일시 정지(Pace and pauses)
모델은 인간 내레이터보다 빠르게 읽습니다. 초당 17.4자 대 14.5자입니다. 15분 정도 걸릴 것으로 예상했던 한 장이 12분에 끝났습니다. 약간 서두르는 오디오북 같았습니다.
문장 간의 일시 정지는 제가 지어낸 것이 아닙니다. 저는 인간 내레이터의 10분 분량을 측정했습니다. 문장 간의 중앙값(median) 일시 정지 시간은 288밀리초였고, 그 10분 동안 0.6초보다 긴 일시 정지는 단 세 번이었습니다. 그래서 제 파이프라인은 문장 사이에 300ms를 넣고, 화자가 바뀔 때는 조금 더 많이, 그리고 단락 사이에는 또 조금 더 많이 넣습니다.
이 모든 과정을 거친 후, 챕터 1의 문자 오류율(character errors)은 2.56%에서 1.84%로 떨어졌습니다. 모델도 같고 가중치(weights)도 같습니다. 바뀐 것은 제가 입력한 것과 그 후에 오디오에 적용한 처리 방식이었습니다.
16개 챕터: 책 전체 길이에서만 존재하는 버그들
그리고 나머지 15개 챕터가 이어졌습니다. 저는 챕터 1을 재렌더링하지 않았습니다. 같은 파이프라인으로 나왔고, 아래에 있는 모든 수정 사항 이후에도 추출된 텍스트는 이전과 바이트 단위로 동일했습니다. 이는 또한 챕터 1이 다음에 오는 내용 중 아무것도 포함하고 있지 않았다는 것을 의미하므로, 한 챕터 테스트로는 그것을 찾아낼 방법이 없었습니다.
장면을 구분하는 세 개의 별표(* * *)는 모델에게 텍스트로 전달되었고, 모델은 이를 충실하게 읽어주었습니다. 소설의 마지막 단락은 작가의 날짜 표기("1940 s.")였습니다. 제 추출 과정(extraction)은 이를 일반 텍스트로 간주하여, 소설이 연도와 함께 읽히며 끝났습니다. 또한, 줄임표가 끊기는 곳마다 다섯 문장이 반으로 잘려나갔습니다.
이 모든 것은 모델의 문제가 아닙니다. 이것은 파이프라인 전체에서 가장 화려하지 않은 코드인 추출(extraction) 과정의 문제입니다. 샘플에 적용하기 전에 전체 텍스트에 걸쳐 실행해 보아야 합니다.
가드(guard)가 7장을 만들기를 거부했고, 그것은 옳았다
패키징 단계에는 가드(guard) 기능이 내장되어 있습니다. 7장의 경우, 이 중 하나의 가드가 장을 만드는 것을 거부했습니다. 한 문장의 단어들을 다시 합쳐도 원래의 문장이 재현되지 않았기 때문입니다. 원인은 책 파일에 포함된 공백 두 개였습니다.
공백 두 개는 들을 수도 없고 화면으로 볼 수도 없습니다. 하지만 텍스트를 단어 단위로 렌더링하는 리더(reader)라면, 소리 없이도 이를 놓쳤을 것입니다. 이 프로젝트에서 프로그램이 실행을 거부한 사례가 포착된 것은 두 번째였으며, 이는 아무리 많이 들어봐도 알 수 없는 것이었습니다.
그 사건은 또한 실행 방식의 변화를 가져왔습니다. 만약 한 장에서 가드가 작동하면(fires), 나머지 장들은 계속 진행됩니다. 실패한 장은 여전히 만들어지지 않지만, 나머지는 그것을 기다리지 않습니다. 밤에 7장에서 멈춘 책은 하루가 손실된 것입니다.
체커(checker) 역시 모델이다
저는 모든 장을 개별적으로 확인했고, 한 장이 이상했습니다. 바로 4장이었는데, 문자 오류율이 8.3%로 책의 중앙값보다 네 배나 높았습니다. 제가 직접 들어보니 그 장은 괜찮았습니다. 이 전체 오류는 "– dep tüşündi."라는 두 단어짜리 한 줄에서 발생했는데, 여기서 검사 인식기(checking recogniser)가 루프에 빠져서 이를 25번이나 연속으로 출력했기 때문입니다. 이 줄만 없다면 그 장은 나머지 부분처럼 약 2% 수준일 것입니다. 같은 줄이 2장에서도 검사를 실패하게 만들었지만, 그곳에서는 눈에 덜 띄었습니다. 책을 읽는 기계(machine)가 고장 난 것이 아니었습니다. 그것을 확인하는 기계(machine)가 고장 난 것이었습니다.
이것 역시 책 전체의 평균을 내는 것이 잘못된 수치가 될 이유입니다. 단 하나의 반복되는 문장도 평균치를 망치고 읽기 경험에 대해서는 아무것도 말해주지 못합니다. 장(chapter)별로 보면 상황은 평탄합니다: 중앙값(median) 2.15%, 최고 장은 1.5%, 최저 장은 2.7%입니다. 품질이 책의 후반부나 긴 장에서 변동하지 않습니다. 가장 긴 두 번째 장은 19분 반 동안 진행되며 2.5%로 읽힙니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기