이미지 로딩 중...
AI Generated
2025. 11. 19. · 4 Views
음성-텍스트 정렬 데이터셋 구축 완벽 가이드
AI 음성 합성과 음성 복제 프로젝트를 위한 고품질 데이터셋 구축 방법을 단계별로 알아봅니다. 음성 파일과 텍스트를 정확하게 정렬하여 실전에서 바로 활용할 수 있는 데이터셋을 만드는 실무 가이드입니다.
목차
- 자동 음성 인식(ASR)을 이용한 전사
- Whisper 모델 활용한 한국어 전사
- 전사 결과 수동 검토 및 수정
- 타임스탬프 기반 정렬 (Forced Alignment)
- (audio, text) 페어 데이터셋 생성
- 데이터셋 품질 검증 체크리스트
1. 자동 음성 인식(ASR)을 이용한 전사
시작하며
여러분이 음성 합성 모델을 학습시키려고 할 때 이런 상황을 겪어본 적 있나요? 수백 개의 음성 파일이 있는데, 각 파일에서 정확히 무슨 말을 하는지 텍스트로 변환하는 작업을 수작업으로 해야 하는 상황 말이죠.
이런 문제는 실제 AI 음성 프로젝트에서 가장 시간이 많이 걸리는 작업 중 하나입니다. 한 시간짜리 음성을 수동으로 전사하려면 보통 4-5시간이 걸립니다.
데이터가 많을수록 이 작업은 비현실적으로 커지죠. 바로 이럴 때 필요한 것이 자동 음성 인식(ASR, Automatic Speech Recognition)입니다.
ASR은 음성 파일을 자동으로 분석해서 텍스트로 변환해주기 때문에, 수백 시간의 수작업을 몇 분으로 단축시킬 수 있습니다.
개요
간단히 말해서, ASR은 사람의 목소리를 듣고 그것을 글자로 바꿔주는 인공지능 기술입니다. 여러분이 스마트폰에서 "음성으로 입력하기"를 사용해본 적이 있다면, 바로 그것이 ASR 기술입니다.
음성-텍스트 정렬 데이터셋을 만들 때 ASR은 첫 번째 단계로 매우 중요합니다. 음성 파일 하나하나를 사람이 듣고 타이핑하는 대신, ASR 모델이 자동으로 음성을 텍스트로 변환해줍니다.
예를 들어, 100개의 5분짜리 오디오북 클립이 있다면, ASR을 사용하면 몇 시간 안에 모든 전사(transcription)를 완료할 수 있습니다. 기존에는 전문 전사 작업자가 오디오를 듣고 수동으로 타이핑했다면, 이제는 ASR 모델에 오디오를 입력하면 자동으로 텍스트가 출력됩니다.
ASR의 핵심 특징은 크게 세 가지입니다: 1) 빠른 처리 속도 - 실시간 또는 그보다 빠르게 전사 가능, 2) 일관성 있는 품질 - 같은 조건이면 항상 같은 결과, 3) 대량 처리 능력 - 수백 시간의 오디오도 자동화 가능. 이러한 특징들이 대규모 데이터셋 구축에서 필수적인 이유입니다.
코드 예제
import whisper
import os
# Whisper 모델 로드 (medium 모델 사용)
model = whisper.load_model("medium")
# 음성 파일 경로
audio_path = "speech_sample.wav"
# 음성 파일 전사 (한국어 지정)
result = model.transcribe(
audio_path,
language="ko", # 한국어로 지정
task="transcribe" # 전사 작업
)
# 전사 결과 출력
print("전사된 텍스트:", result["text"])
# 세그먼트별 정보도 확인 가능
for segment in result["segments"]:
print(f"[{segment['start']:.2f}s - {segment['end']:.2f}s] {segment['text']}")
설명
이것이 하는 일: 위 코드는 Whisper라는 강력한 ASR 모델을 사용해서 음성 파일을 한국어 텍스트로 자동 변환합니다. 첫 번째로, whisper.load_model("medium")은 사전 학습된 Whisper 모델을 메모리에 불러옵니다.
Whisper는 OpenAI가 만든 최신 ASR 모델로, 다국어를 지원하며 특히 한국어에서도 높은 정확도를 보입니다. "medium" 크기는 속도와 정확도의 균형이 좋아서 실무에서 가장 많이 사용됩니다.
그 다음으로, model.transcribe() 함수가 실행되면서 음성 파일을 분석합니다. 내부에서는 음성을 작은 시간 단위로 나누고, 각 부분에서 음향 특징을 추출한 뒤, 딥러닝 모델이 이것을 텍스트로 변환합니다.
language="ko" 파라미터를 지정하면 한국어 언어 모델을 사용하므로 정확도가 크게 향상됩니다. 마지막으로, result 객체에는 전체 전사 텍스트뿐만 아니라 세그먼트별 시간 정보도 포함됩니다.
각 세그먼트는 몇 초부터 몇 초까지 어떤 텍스트가 말해졌는지를 담고 있어, 나중에 정밀한 정렬 작업에 매우 유용합니다. 여러분이 이 코드를 사용하면 음성 파일을 자동으로 텍스트로 변환할 수 있고, 시간 정보까지 얻을 수 있으며, 대량의 데이터도 for 루프로 쉽게 처리할 수 있습니다.
실무에서는 배치 처리 스크립트로 확장하여 수백 개의 파일을 한 번에 처리하는 것이 일반적입니다.
실전 팁
💡 모델 크기는 tiny < base < small < medium < large 순으로 커지며, 정확도가 높아지지만 처리 속도는 느려집니다. 실험용으로는 small, 실전용으로는 medium 이상을 추천합니다.
💡 배경 소음이 많은 음성의 경우, 전처리로 노이즈 제거(librosa, noisereduce 라이브러리)를 먼저 수행하면 전사 정확도가 크게 향상됩니다.
💡 GPU가 있다면 자동으로 사용되어 10배 이상 빠른 처리가 가능합니다. torch.cuda.is_available()로 GPU 사용 여부를 확인하세요.
💡 긴 오디오(30분 이상)는 메모리 문제가 발생할 수 있으니 파일을 5-10분 단위로 나누어 처리하는 것이 안전합니다.
💡 fp16=False 옵션을 추가하면 정밀도가 높아져 품질이 좋아지지만 속도는 약간 느려집니다. 품질이 중요한 프로젝트에서 활용하세요.
2. Whisper 모델 활용한 한국어 전사
시작하며
여러분이 한국어 음성 데이터로 TTS 모델을 만들려고 할 때 이런 고민을 해본 적 있나요? 영어는 ASR 모델이 많은데, 한국어는 어떤 모델을 써야 정확한 전사를 얻을 수 있을까 하는 고민 말이죠.
이런 문제는 비영어권 AI 개발자들이 공통적으로 겪는 어려움입니다. 예전 ASR 모델들은 주로 영어에 최적화되어 있어서, 한국어 전사 정확도가 70-80% 수준에 머물렀습니다.
이 정도면 수정 작업에 오히려 더 많은 시간이 걸리게 됩니다. 바로 이럴 때 필요한 것이 Whisper 모델입니다.
Whisper는 99개 언어를 지원하며, 특히 한국어에서도 95% 이상의 높은 정확도를 보여줘서 실무에서 바로 사용할 수 있는 품질의 전사를 생성합니다.
개요
간단히 말해서, Whisper는 OpenAI가 68만 시간의 다국어 음성 데이터로 학습시킨 범용 ASR 모델입니다. 특히 한국어를 포함한 비영어권 언어에서도 상업적 수준의 전사 품질을 제공합니다.
한국어 음성 데이터셋을 만들 때 Whisper는 가장 신뢰할 수 있는 선택지입니다. 다른 한국어 전용 ASR 모델들도 있지만, Whisper는 설치가 쉽고, 무료이며, 성능이 우수해서 많은 기업과 연구자들이 사용합니다.
예를 들어, K-Pop 가사 데이터셋, 드라마 대사 데이터셋, 오디오북 데이터셋을 만들 때 모두 Whisper를 사용할 수 있습니다. 기존에는 한국어 전사를 위해 Google Cloud Speech-to-Text 같은 유료 API를 사용했다면, 이제는 Whisper를 로컬에서 무료로 실행하면서도 비슷하거나 더 나은 품질을 얻을 수 있습니다.
Whisper의 핵심 특징은: 1) 높은 한국어 정확도 - 일상 대화, 뉴스, 강의 등 다양한 도메인에서 우수한 성능, 2) 시간 정렬 정보 제공 - 각 단어/문장의 시작/끝 시간까지 제공, 3) 로컬 실행 가능 - 인터넷 연결 없이도 작동하고 데이터 프라이버시 보장. 이러한 특징들이 한국어 음성 AI 프로젝트에서 Whisper를 표준처럼 사용하게 만듭니다.
코드 예제
import whisper
import json
from pathlib import Path
def transcribe_korean_audio(audio_path, output_json_path):
"""한국어 음성을 전사하고 결과를 JSON으로 저장"""
# large-v2 모델 사용 (한국어 정확도 최고)
model = whisper.load_model("large-v2")
# 한국어 전사 실행 (상세 옵션 설정)
result = model.transcribe(
audio_path,
language="ko",
task="transcribe",
verbose=True, # 진행 상황 출력
word_timestamps=True # 단어별 타임스탬프 생성
)
# 결과를 구조화된 형태로 저장
output_data = {
"audio_file": str(audio_path),
"full_text": result["text"],
"segments": [
{
"id": seg["id"],
"start": seg["start"],
"end": seg["end"],
"text": seg["text"].strip()
}
for seg in result["segments"]
]
}
# JSON 파일로 저장
with open(output_json_path, "w", encoding="utf-8") as f:
json.dump(output_data, f, ensure_ascii=False, indent=2)
return output_data
# 실행 예시
result = transcribe_korean_audio(
"korean_speech.wav",
"transcription_result.json"
)
print(f"전사 완료: {len(result['segments'])}개 세그먼트 생성")
설명
이것이 하는 일: 위 코드는 한국어 음성 파일을 Whisper의 가장 강력한 모델로 전사하고, 결과를 세그먼트별로 구조화하여 JSON 파일로 저장합니다. 첫 번째로, whisper.load_model("large-v2")는 Whisper의 가장 큰 모델을 불러옵니다.
large-v2는 파라미터 수가 15억 개로, 한국어 전사에서 최고 수준의 정확도를 제공합니다. 조금 느리더라도 품질이 중요한 데이터셋 구축에서는 이 모델을 추천합니다.
그 다음으로, model.transcribe() 함수에서 word_timestamps=True 옵션이 매우 중요합니다. 이 옵션을 켜면 문장 단위뿐만 아니라 각 단어의 시작과 끝 시간까지 알 수 있어서, 나중에 forced alignment나 립싱크 작업에 직접 활용할 수 있습니다.
verbose=True는 진행 상황을 실시간으로 보여줘서 긴 파일 처리 시 유용합니다. 마지막으로, 전사 결과를 JSON 형태로 구조화합니다.
각 세그먼트는 ID, 시작 시간, 끝 시간, 텍스트를 포함하는 딕셔너리로 저장되어, 나중에 다른 프로그램에서 쉽게 읽고 처리할 수 있습니다. ensure_ascii=False는 한글이 깨지지 않도록 UTF-8로 저장하는 필수 옵션입니다.
여러분이 이 코드를 사용하면 한국어 음성을 고품질로 전사할 수 있고, 시간 정보가 포함된 구조화된 데이터를 얻을 수 있으며, JSON 형식이라 데이터베이스 저장이나 후처리가 매우 쉬워집니다. 실무에서는 이 함수를 여러 파일에 반복 적용하여 대량의 데이터셋을 자동으로 구축합니다.
실전 팁
💡 large-v2 모델은 GPU 메모리를 10GB 이상 사용하므로, GPU가 부족하다면 medium 모델로 시작해도 한국어는 충분히 좋은 품질을 얻을 수 있습니다.
💡 initial_prompt 파라미터에 예상 문맥을 제공하면 정확도가 더 높아집니다. 예: initial_prompt="이것은 기술 강의 내용입니다"
💡 배경 음악이 있는 음성(예: 드라마, 영화)은 전사 품질이 떨어지므로, 가능하면 보컬 추출 도구(spleeter, demucs)로 음성만 분리하세요.
💡 JSON 결과를 pandas DataFrame으로 변환하면 데이터 분석과 필터링이 훨씬 편리합니다: pd.DataFrame(result['segments'])
💡 동일한 화자가 여러 파일을 녹음한 경우, 첫 번째 파일의 전사 결과 일부를 initial_prompt로 사용하면 나머지 파일의 정확도가 향상됩니다.
3. 전사 결과 수동 검토 및 수정
시작하며
여러분이 ASR로 음성을 전사한 후 이런 상황을 발견한 적 있나요? "안녕하세요"가 "안녕 하세요"로 띄어쓰기가 틀리거나, "강화학습"이 "강화 학습"으로 잘못 인식되는 경우 말이죠.
이런 문제는 아무리 좋은 ASR 모델이라도 피할 수 없습니다. Whisper의 정확도가 95%라는 것은, 100개 단어 중 5개는 틀릴 수 있다는 의미입니다.
특히 전문 용어, 고유명사, 숫자, 외래어 같은 경우 오류율이 더 높아집니다. 이런 오류를 그대로 두고 모델을 학습시키면, 학습된 TTS 모델도 이상한 발음을 배우게 됩니다.
바로 이럴 때 필요한 것이 전사 결과의 수동 검토 및 수정입니다. 전문가가 전사 결과를 들으면서 확인하고 수정하는 이 단계가, 고품질 데이터셋을 만드는 가장 중요한 품질 관리 포인트입니다.
개요
간단히 말해서, 수동 검토는 ASR이 생성한 텍스트를 사람이 직접 음성을 들으면서 확인하고 틀린 부분을 수정하는 작업입니다. 마치 작가가 쓴 글을 편집자가 교정하는 것과 같은 과정입니다.
수동 검토가 필요한 이유는 AI 모델 학습에는 "쓰레기가 들어가면 쓰레기가 나온다(Garbage In, Garbage Out)"는 원칙이 있기 때문입니다. 전사 오류가 5%만 있어도, 1000개 문장 중 50개가 틀린 것이고, 이것으로 학습한 TTS 모델은 발음이 부정확하거나 이상한 억양을 배울 수 있습니다.
예를 들어, 음성 복제 프로젝트에서 유명인의 목소리를 학습시킨다면, 전사의 정확도가 곧 복제 품질을 결정합니다. 기존에는 전문 전사 업체에 의뢰하거나 크라우드소싱 플랫폼을 사용했다면, 이제는 ASR로 초안을 빠르게 만들고 전문가가 검토만 하는 하이브리드 방식으로 시간과 비용을 크게 절감할 수 있습니다.
수동 검토의 핵심 포인트는: 1) 정확성 - 모든 단어가 실제 발화와 일치해야 함, 2) 일관성 - 같은 단어는 항상 같은 표기 사용(예: "딥러닝" vs "딥 러닝"), 3) 완전성 - 숨소리, 더듬거림 등 불필요한 소리는 제거하되 의미 있는 모든 발화는 포함. 이러한 기준들이 데이터셋의 품질을 결정합니다.
코드 예제
import json
import soundfile as sf
import sounddevice as sd
from pathlib import Path
class TranscriptionReviewer:
"""전사 결과 검토 및 수정을 위한 간단한 도구"""
def __init__(self, audio_path, transcription_path):
self.audio_path = audio_path
self.audio_data, self.sample_rate = sf.read(audio_path)
with open(transcription_path, 'r', encoding='utf-8') as f:
self.transcription = json.load(f)
def play_segment(self, segment_index):
"""특정 세그먼트의 오디오 재생"""
segment = self.transcription['segments'][segment_index]
start_sample = int(segment['start'] * self.sample_rate)
end_sample = int(segment['end'] * self.sample_rate)
# 해당 구간만 재생
sd.play(self.audio_data[start_sample:end_sample], self.sample_rate)
sd.wait()
def review_and_edit(self):
"""대화형 검토 프로세스"""
for i, segment in enumerate(self.transcription['segments']):
print(f"\n[세그먼트 {i+1}/{len(self.transcription['segments'])}]")
print(f"시간: {segment['start']:.2f}s - {segment['end']:.2f}s")
print(f"현재 텍스트: {segment['text']}")
# 오디오 재생
self.play_segment(i)
# 수정 여부 확인
action = input("수정하시겠습니까? (y/n/r=다시듣기): ").lower()
if action == 'r':
self.play_segment(i)
action = input("수정하시겠습니까? (y/n): ").lower()
if action == 'y':
new_text = input("수정된 텍스트 입력: ")
segment['text'] = new_text
segment['manually_reviewed'] = True
print("✓ 수정 완료")
else:
segment['manually_reviewed'] = True
print("✓ 원본 유지")
return self.transcription
def save_reviewed(self, output_path):
"""검토 완료된 전사 결과 저장"""
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(self.transcription, f, ensure_ascii=False, indent=2)
print(f"\n검토 완료! 저장됨: {output_path}")
# 사용 예시
reviewer = TranscriptionReviewer(
"korean_speech.wav",
"transcription_result.json"
)
reviewed_data = reviewer.review_and_edit()
reviewer.save_reviewed("transcription_reviewed.json")
설명
이것이 하는 일: 위 코드는 전사 결과를 세그먼트별로 재생하면서 검토하고, 틀린 부분을 바로 수정할 수 있는 대화형 도구를 제공합니다. 첫 번째로, TranscriptionReviewer 클래스는 오디오 파일과 전사 JSON을 함께 불러옵니다.
soundfile 라이브러리로 오디오 데이터를 numpy 배열로 읽어서, 나중에 특정 시간 구간만 잘라서 재생할 수 있게 준비합니다. 이렇게 하면 검토자가 긴 파일을 처음부터 끝까지 듣지 않고, 문제가 있는 부분만 집중적으로 들을 수 있습니다.
그 다음으로, play_segment() 메서드가 핵심입니다. 세그먼트의 시작 시간과 끝 시간을 샘플 인덱스로 변환하여 (시간 × 샘플레이트), 해당 구간의 오디오만 정확하게 재생합니다.
예를 들어 3.5초부터 7.2초까지의 발화만 들을 수 있어서, 검토 작업이 매우 효율적입니다. 마지막으로, review_and_edit() 메서드는 대화형 루프를 실행합니다.
각 세그먼트를 재생한 후 사용자에게 수정 여부를 물어보고, 수정이 필요하면 새 텍스트를 입력받습니다. manually_reviewed 플래그를 추가하여 어떤 세그먼트가 검토되었는지 추적할 수 있어, 나중에 검토율을 계산하거나 미검토 부분만 다시 작업할 수 있습니다.
여러분이 이 코드를 사용하면 전사 검토 작업을 체계적으로 진행할 수 있고, 검토 이력이 데이터에 기록되며, 팀으로 작업할 때 누가 어디까지 검토했는지 추적할 수 있습니다. 실무에서는 이것을 웹 인터페이스로 확장하여 여러 검토자가 동시에 작업할 수 있게 만듭니다.
실전 팁
💡 검토 작업은 피로가 쌓이면 정확도가 떨어지므로, 1시간 작업 후 10-15분 휴식을 권장합니다. 집중력이 떨어진 상태의 검토는 오히려 오류를 추가할 수 있습니다.
💡 전문 용어나 고유명사가 많은 도메인이라면, 사전에 용어집(glossary)을 만들어 검토자들이 참고하게 하면 일관성이 크게 향상됩니다.
💡 재생 속도를 조절할 수 있는 기능을 추가하면 검토 속도가 빨라집니다. 명확한 발화는 1.5배속으로, 불명확한 부분은 0.75배속으로 들으세요.
💡 두 명 이상의 검토자가 같은 세그먼트를 독립적으로 검토하고 결과를 비교하면(inter-annotator agreement), 품질을 객관적으로 측정할 수 있습니다.
💡 Label Studio, Audacity, Praat 같은 전문 도구를 사용하면 파형을 보면서 검토할 수 있어 더 정확한 수정이 가능합니다.
4. 타임스탬프 기반 정렬 (Forced Alignment)
시작하며
여러분이 음성과 텍스트를 정확히 매칭하려고 할 때 이런 상황을 겪어본 적 있나요? Whisper가 "3.5초에서 7.2초까지 이 문장이 말해졌다"고 알려주는데, 실제로는 각 음소나 단어가 정확히 언제 시작하고 끝나는지 더 세밀하게 알아야 하는 경우 말이죠.
이런 문제는 고품질 TTS 모델을 학습시킬 때 특히 중요합니다. 음성 합성 모델은 "이 텍스트의 이 부분이 오디오의 정확히 이 프레임에 대응된다"는 정밀한 정렬 정보를 필요로 합니다.
문장 단위 정렬만으로는 부족하고, 음소(phoneme) 또는 단어 단위의 정확한 타이밍이 필요합니다. 바로 이럴 때 필요한 것이 Forced Alignment입니다.
Forced Alignment는 이미 알고 있는 텍스트와 음성을 음소 단위로 정확하게 매칭시켜주는 기술로, TTS 데이터셋 구축의 핵심 단계입니다.
개요
간단히 말해서, Forced Alignment는 음성 파일과 그 텍스트가 주어졌을 때, 각 음소나 단어가 정확히 몇 초 몇 밀리초에 발화되었는지 자동으로 찾아주는 기술입니다. 마치 노래방 자막이 가사와 완벽하게 동기화되는 것처럼, 텍스트의 각 부분을 음성의 정확한 시간에 매핑합니다.
Forced Alignment가 중요한 이유는 TTS 모델(특히 Tacotron, FastSpeech 같은 모델)이 학습할 때 텍스트-음성 쌍의 정확한 정렬을 필요로 하기 때문입니다. 예를 들어, "안녕하세요"라는 단어를 합성할 때, 모델은 "ㅏ" 음소가 어떤 스펙트로그램 프레임에 대응되는지 정확히 알아야 자연스러운 음성을 생성할 수 있습니다.
기존에는 수작업으로 Praat 같은 도구를 사용해 음소별로 경계를 표시했다면, 이제는 Montreal Forced Aligner(MFA)나 다른 자동 정렬 도구로 수천 개의 파일을 몇 시간 만에 자동 정렬할 수 있습니다. Forced Alignment의 핵심 요소는: 1) 음소 인식 - 텍스트를 음소 시퀀스로 변환, 2) 음향 모델 - 오디오에서 각 음소의 특징을 인식, 3) 동적 정렬 - Viterbi 알고리즘 등으로 최적의 정렬 경로 찾기.
이러한 과정들이 결합되어 밀리초 단위의 정확한 정렬을 만들어냅니다.
코드 예제
from montreal_forced_aligner import align
import json
from pathlib import Path
def forced_align_korean(audio_path, text_path, output_path):
"""
Montreal Forced Aligner를 사용한 한국어 음성-텍스트 정렬
"""
# MFA 실행 (한국어 사전 및 음향 모델 사용)
# 사전에 MFA 설치 및 한국어 모델 다운로드 필요:
# mfa model download acoustic korean_mfa
# mfa model download dictionary korean_mfa
align_result = align(
corpus_dir="./audio_corpus", # 오디오와 텍스트 파일들이 있는 폴더
dictionary_path="korean_mfa", # 한국어 발음 사전
acoustic_model_path="korean_mfa", # 한국어 음향 모델
output_dir="./aligned_output" # 정렬 결과 저장 폴더
)
return align_result
def parse_textgrid_to_json(textgrid_path, output_json_path):
"""
MFA가 생성한 TextGrid 파일을 JSON으로 변환
"""
import tgt # TextGrid Tools 라이브러리
# TextGrid 파일 읽기
tg = tgt.read_textgrid(textgrid_path)
# 단어 및 음소 정렬 정보 추출
result = {
"words": [],
"phones": []
}
# 단어 tier 처리
word_tier = tg.get_tier_by_name("words")
for interval in word_tier:
if interval.text: # 빈 구간 제외
result["words"].append({
"word": interval.text,
"start": round(interval.start_time, 3),
"end": round(interval.end_time, 3),
"duration": round(interval.end_time - interval.start_time, 3)
})
# 음소 tier 처리
phone_tier = tg.get_tier_by_name("phones")
for interval in phone_tier:
if interval.text:
result["phones"].append({
"phone": interval.text,
"start": round(interval.start_time, 3),
"end": round(interval.end_time, 3),
"duration": round(interval.end_time - interval.start_time, 3)
})
# JSON으로 저장
with open(output_json_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"정렬 완료: {len(result['words'])}개 단어, {len(result['phones'])}개 음소")
return result
# 실행 예시
alignment = parse_textgrid_to_json(
"aligned_output/korean_speech.TextGrid",
"alignment_result.json"
)
설명
이것이 하는 일: 위 코드는 Montreal Forced Aligner(MFA)를 사용해 한국어 음성과 텍스트를 음소 단위로 자동 정렬하고, 결과를 JSON 형식으로 변환합니다. 첫 번째로, forced_align_korean() 함수는 MFA의 align() 함수를 호출합니다.
MFA는 음성 인식 기술을 역으로 사용하여, "이 텍스트가 이 음성에 포함되어 있다"는 것을 알고 있는 상태에서 각 음소의 정확한 위치를 찾아냅니다. 내부적으로는 HMM(Hidden Markov Model)과 GMM(Gaussian Mixture Model)을 사용하여 음향 특징과 음소를 매칭합니다.
한국어 사전(korean_mfa)은 각 한글 단어를 음소로 변환하는 규칙을 포함하고 있습니다. 그 다음으로, MFA는 결과를 TextGrid 형식으로 출력합니다.
TextGrid는 음성학 연구에서 표준으로 사용되는 형식으로, tier(계층)라는 개념으로 단어 정렬과 음소 정렬을 구분합니다. 하지만 프로그래밍에서 사용하기 어려우므로, parse_textgrid_to_json() 함수로 JSON 형식으로 변환합니다.
마지막으로, JSON 결과에는 각 단어와 음소의 시작 시간, 끝 시간, 지속 시간이 밀리초 단위로 기록됩니다. 예를 들어, "안녕하세요"라는 단어가 1.250초에서 2.180초까지 발화되었고, 그 안에서 "ㅏ" 음소는 1.250초에서 1.320초까지 지속되었다는 정보를 얻을 수 있습니다.
이 정보는 TTS 모델 학습 시 어텐션 메커니즘의 정답 레이블로 사용됩니다. 여러분이 이 코드를 사용하면 대량의 음성 파일을 자동으로 정밀 정렬할 수 있고, TTS 모델이 요구하는 형식의 데이터를 생성할 수 있으며, 정렬 품질을 시각화하거나 검증하는 후처리 작업도 쉽게 할 수 있습니다.
실무에서는 배치 처리로 수천 개 파일을 한 번에 정렬합니다.
실전 팁
💡 MFA는 설치가 복잡할 수 있으므로, Docker 이미지를 사용하면 환경 설정 문제를 피할 수 있습니다: docker pull mmcauliffe/montreal-forced-aligner
💡 배경 소음이 많거나 발화가 불명확한 부분은 정렬 품질이 떨어지므로, 정렬 후 confidence score가 낮은 세그먼트는 수동 검토가 필요합니다.
💡 MFA 외에도 Kaldi, Gentle 같은 도구도 있지만, MFA가 가장 설치가 쉽고 한국어 지원이 좋아서 추천합니다.
💡 정렬 결과를 Praat으로 열어서 파형과 함께 시각화하면, 정렬 정확도를 빠르게 확인할 수 있습니다.
💡 같은 화자의 여러 파일을 정렬할 때는 화자별로 음향 모델을 fine-tuning하면 정렬 정확도가 향상됩니다(MFA의 speaker adaptation 기능 활용).
5. (audio, text) 페어 데이터셋 생성
시작하며
여러분이 지금까지 음성 전사, 검토, 정렬까지 마쳤다면 이런 고민을 하게 됩니다. "이 모든 정보를 어떻게 정리해서 실제 모델 학습에 사용할 수 있는 형태로 만들까?" 이런 문제는 데이터 엔지니어링의 마지막 단계에서 매우 중요합니다.
아무리 좋은 원본 데이터가 있어도, 모델이 읽을 수 있는 형식으로 정리되지 않으면 사용할 수 없습니다. TTS 모델들은 각자 다른 데이터 형식을 요구하지만, 기본적으로는 (오디오 파일 경로, 텍스트) 쌍의 리스트 형태를 필요로 합니다.
바로 이럴 때 필요한 것이 표준화된 페어 데이터셋 생성입니다. 모든 정보를 하나의 메타데이터 파일로 통합하고, 오디오 파일을 적절히 분할하여, 모델이 바로 학습에 사용할 수 있는 형태로 만드는 과정입니다.
개요
간단히 말해서, 페어 데이터셋은 각 오디오 클립과 대응하는 텍스트를 쌍으로 묶은 구조화된 데이터셋입니다. 마치 교과서의 문제와 정답이 쌍으로 되어 있는 것처럼, 모델에게 "이 소리는 이 글자를 의미한다"고 가르치는 학습 자료입니다.
페어 데이터셋 생성이 중요한 이유는 모델 학습의 효율성과 품질을 직접적으로 결정하기 때문입니다. 데이터셋이 잘 구성되어 있으면 학습이 빠르고, 데이터 로딩 오류가 없으며, 재현 가능한 실험이 가능합니다.
예를 들어, LJSpeech, KSS 같은 유명한 공개 TTS 데이터셋들은 모두 metadata.csv 파일로 (파일명|텍스트) 형태를 제공합니다. 기존에는 엑셀 파일이나 텍스트 파일로 수동 관리했다면, 이제는 스크립트로 자동화하여 CSV, JSON, 또는 데이터베이스 형태로 체계적으로 관리할 수 있습니다.
페어 데이터셋의 핵심 구성 요소는: 1) 메타데이터 파일 - 모든 샘플의 정보를 담은 인덱스, 2) 오디오 파일들 - 적절한 길이로 분할된 음성 클립들, 3) 텍스트 정규화 - 숫자, 기호 등을 읽는 방식으로 변환. 이러한 요소들이 모두 일관성 있게 구성되어야 합니다.
코드 예제
import pandas as pd
import json
from pathlib import Path
import soundfile as sf
import numpy as np
class TTSDatasetBuilder:
"""TTS 학습용 페어 데이터셋 생성 클래스"""
def __init__(self, output_dir):
self.output_dir = Path(output_dir)
self.output_dir.mkdir(exist_ok=True)
(self.output_dir / "wavs").mkdir(exist_ok=True)
self.metadata = []
def add_sample_from_alignment(self, audio_path, alignment_json_path,
min_duration=1.0, max_duration=10.0):
"""
정렬 결과를 바탕으로 샘플 추가
"""
# 오디오 로드
audio_data, sr = sf.read(audio_path)
# 정렬 정보 로드
with open(alignment_json_path, 'r', encoding='utf-8') as f:
alignment = json.load(f)
# 문장별로 오디오 분할
for i, segment in enumerate(alignment.get('segments', [])):
duration = segment['end'] - segment['start']
# 너무 짧거나 긴 샘플은 제외
if duration < min_duration or duration > max_duration:
continue
# 오디오 분할
start_sample = int(segment['start'] * sr)
end_sample = int(segment['end'] * sr)
audio_clip = audio_data[start_sample:end_sample]
# 파일명 생성 (원본파일명_세그먼트번호)
audio_basename = Path(audio_path).stem
clip_filename = f"{audio_basename}_{i:04d}.wav"
clip_path = self.output_dir / "wavs" / clip_filename
# 오디오 클립 저장
sf.write(clip_path, audio_clip, sr)
# 텍스트 정규화
text = self.normalize_text(segment['text'])
# 메타데이터 추가
self.metadata.append({
"audio_file": f"wavs/{clip_filename}",
"text": text,
"duration": round(duration, 2),
"sample_rate": sr
})
def normalize_text(self, text):
"""텍스트 정규화 (숫자, 특수문자 처리)"""
# 간단한 정규화 예시
text = text.strip()
# 실제로는 더 복잡한 정규화 필요 (숫자→한글, 영어 처리 등)
# 예: "100원" → "백원", "Dr." → "닥터"
return text
def save_metadata(self, format='csv'):
"""메타데이터 저장"""
df = pd.DataFrame(self.metadata)
if format == 'csv':
# LJSpeech 스타일: 파일명|텍스트|정규화된텍스트
csv_path = self.output_dir / "metadata.csv"
with open(csv_path, 'w', encoding='utf-8') as f:
for _, row in df.iterrows():
f.write(f"{row['audio_file']}|{row['text']}\n")
print(f"CSV 저장 완료: {csv_path}")
elif format == 'json':
json_path = self.output_dir / "metadata.json"
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(self.metadata, f, ensure_ascii=False, indent=2)
print(f"JSON 저장 완료: {json_path}")
# 통계 출력
print(f"\n데이터셋 생성 완료:")
print(f" - 총 샘플 수: {len(self.metadata)}")
print(f" - 총 길이: {df['duration'].sum() / 60:.1f}분")
print(f" - 평균 길이: {df['duration'].mean():.2f}초")
# 사용 예시
builder = TTSDatasetBuilder("./tts_dataset")
builder.add_sample_from_alignment(
"korean_speech.wav",
"alignment_result.json"
)
builder.save_metadata(format='csv')
설명
이것이 하는 일: 위 코드는 정렬 정보를 바탕으로 긴 오디오를 문장 단위로 분할하고, 각 클립과 텍스트를 쌍으로 묶어 표준 형식의 TTS 데이터셋을 생성합니다. 첫 번째로, TTSDatasetBuilder 클래스는 데이터셋의 디렉토리 구조를 생성합니다.
output_dir/wavs/ 폴더에는 모든 오디오 클립이 저장되고, output_dir/metadata.csv에는 각 클립의 정보가 기록됩니다. 이 구조는 LJSpeech, KSS 같은 표준 데이터셋과 동일하여, 대부분의 TTS 코드가 바로 사용할 수 있습니다.
그 다음으로, add_sample_from_alignment() 메서드가 핵심 로직을 담당합니다. 정렬 JSON에서 각 세그먼트의 시간 정보를 읽고, 원본 오디오에서 해당 구간만 추출하여 개별 WAV 파일로 저장합니다.
min_duration과 max_duration 파라미터로 너무 짧거나 긴 샘플을 필터링하는데, 이는 모델 학습 안정성에 중요합니다. 대부분의 TTS 모델은 2-10초 길이의 샘플에서 가장 잘 학습됩니다.
마지막으로, save_metadata() 메서드는 모든 샘플 정보를 CSV 또는 JSON 형식으로 저장합니다. CSV 형식은 파일명|텍스트 구조로, 많은 TTS 코드가 기대하는 표준 형식입니다.
또한 총 샘플 수, 총 길이, 평균 길이 같은 통계를 출력하여 데이터셋 품질을 한눈에 파악할 수 있게 합니다. 여러분이 이 코드를 사용하면 여러 음성 파일을 자동으로 데이터셋으로 변환할 수 있고, Tacotron2, FastSpeech, VITS 같은 대부분의 TTS 모델이 바로 로드할 수 있는 형식을 얻을 수 있으며, 데이터셋 버전 관리와 재현 가능한 실험이 가능해집니다.
실무에서는 이 클래스를 확장하여 다양한 화자, 감정, 스타일을 메타데이터에 추가합니다.
실전 팁
💡 오디오 샘플링 레이트는 22050Hz 또는 24000Hz로 통일하는 것이 TTS 모델 학습에 표준입니다. 원본이 다른 샘플링 레이트라면 resampling하세요.
💡 텍스트 정규화는 매우 중요합니다. "123"을 "백이십삼"으로, "$10"를 "십 달러"로 변환하는 규칙을 체계적으로 구축하세요. 한국어는 KoG2P 라이브러리를 활용할 수 있습니다.
💡 데이터셋을 train/validation/test로 나눌 때, 같은 화자의 같은 문장이 여러 세트에 걸치지 않도록 주의하세요. 보통 8:1:1 비율을 사용합니다.
💡 메타데이터에 화자 ID, 감정 레이블 같은 추가 정보를 포함하면, multi-speaker TTS나 감정 TTS 모델을 학습할 수 있습니다.
💡 데이터셋 크기는 최소 30분 이상을 권장하며, 고품질 결과를 원한다면 1시간 이상이 필요합니다. 단, 품질이 양보다 중요하므로 불량 샘플은 과감히 제거하세요.
6. 데이터셋 품질 검증 체크리스트
시작하며
여러분이 데이터셋을 완성한 후 바로 모델 학습을 시작하려고 할 때 이런 불안감을 느낀 적 있나요? "이 데이터셋으로 학습하면 정말 좋은 결과가 나올까?
혹시 놓친 오류가 있지는 않을까?" 이런 문제는 실제로 매우 흔합니다. 데이터셋에 품질 문제가 있으면, 며칠씩 학습시킨 후에야 결과가 좋지 않다는 것을 알게 되고, 다시 데이터를 수정해서 처음부터 학습을 반복해야 합니다.
이는 시간과 컴퓨팅 자원의 엄청난 낭비입니다. 바로 이럴 때 필요한 것이 데이터셋 품질 검증입니다.
학습을 시작하기 전에 체계적인 체크리스트로 데이터를 검증하여, 문제를 조기에 발견하고 수정하는 것이 훨씬 효율적입니다.
개요
간단히 말해서, 데이터셋 품질 검증은 완성된 데이터셋이 모델 학습에 적합한지 확인하는 종합 검사 과정입니다. 마치 건물을 짓고 난 후 안전 검사를 하는 것처럼, 데이터셋의 완성도를 다각도로 점검합니다.
품질 검증이 중요한 이유는 "쓰레기 데이터로 학습하면 쓰레기 모델이 나온다"는 원칙 때문입니다. 데이터셋의 작은 결함도 학습 과정에서 증폭되어 큰 문제를 일으킬 수 있습니다.
예를 들어, 샘플 중 10%에서 오디오와 텍스트가 불일치하면, 모델은 혼란스러워서 수렴하지 못하거나 이상한 패턴을 학습할 수 있습니다. 기존에는 경험에 의존하여 주먹구구식으로 확인했다면, 이제는 자동화된 스크립트로 정량적 지표를 계산하고, 체계적인 체크리스트로 빠짐없이 검증할 수 있습니다.
품질 검증의 핵심 영역은: 1) 데이터 일관성 - 모든 파일이 존재하고 형식이 올바른지, 2) 음성 품질 - 노이즈, 클리핑, 무음 구간 등의 문제, 3) 텍스트 품질 - 오타, 불일치, 정규화 오류, 4) 정렬 품질 - 오디오와 텍스트가 실제로 일치하는지, 5) 분포 균형 - 길이, 화자, 내용의 편향성. 이러한 영역들을 모두 통과해야 고품질 데이터셋입니다.
코드 예제
import pandas as pd
import soundfile as sf
import numpy as np
from pathlib import Path
import warnings
class DatasetQualityChecker:
"""TTS 데이터셋 품질 검증 도구"""
def __init__(self, dataset_dir, metadata_file="metadata.csv"):
self.dataset_dir = Path(dataset_dir)
self.metadata_path = self.dataset_dir / metadata_file
self.issues = []
def check_all(self):
"""모든 품질 검사 실행"""
print("=== 데이터셋 품질 검증 시작 ===\n")
# 1. 메타데이터 로드
self.load_metadata()
# 2. 파일 존재성 확인
self.check_file_existence()
# 3. 오디오 품질 확인
self.check_audio_quality()
# 4. 텍스트 품질 확인
self.check_text_quality()
# 5. 데이터 분포 확인
self.check_data_distribution()
# 6. 최종 리포트
self.generate_report()
def load_metadata(self):
"""메타데이터 로드"""
try:
self.df = pd.read_csv(
self.metadata_path,
sep='|',
names=['audio_file', 'text'],
encoding='utf-8'
)
print(f"✓ 메타데이터 로드: {len(self.df)}개 샘플")
except Exception as e:
self.issues.append(f"메타데이터 로드 실패: {e}")
raise
def check_file_existence(self):
"""모든 오디오 파일이 존재하는지 확인"""
print("\n[1] 파일 존재성 확인 중...")
missing_files = []
for idx, row in self.df.iterrows():
audio_path = self.dataset_dir / row['audio_file']
if not audio_path.exists():
missing_files.append(row['audio_file'])
if missing_files:
self.issues.append(f"누락된 파일 {len(missing_files)}개: {missing_files[:5]}")
print(f" ✗ 누락된 파일: {len(missing_files)}개")
else:
print(f" ✓ 모든 파일 존재 확인")
def check_audio_quality(self):
"""오디오 품질 확인"""
print("\n[2] 오디오 품질 확인 중...")
short_samples = []
long_samples = []
clipping_samples = []
silent_samples = []
for idx, row in self.df.iterrows():
audio_path = self.dataset_dir / row['audio_file']
if not audio_path.exists():
continue
try:
audio, sr = sf.read(audio_path)
duration = len(audio) / sr
# 너무 짧거나 긴 샘플
if duration < 1.0:
short_samples.append((row['audio_file'], duration))
elif duration > 15.0:
long_samples.append((row['audio_file'], duration))
# 클리핑 (진폭이 ±1.0에 닿음)
if np.max(np.abs(audio)) > 0.99:
clipping_samples.append(row['audio_file'])
# 거의 무음인 샘플
if np.max(np.abs(audio)) < 0.01:
silent_samples.append(row['audio_file'])
except Exception as e:
self.issues.append(f"오디오 읽기 실패: {row['audio_file']}")
# 결과 출력
if short_samples:
print(f" ! 너무 짧은 샘플 (<1초): {len(short_samples)}개")
self.issues.append(f"짧은 샘플: {short_samples[:3]}")
if long_samples:
print(f" ! 너무 긴 샘플 (>15초): {len(long_samples)}개")
self.issues.append(f"긴 샘플: {long_samples[:3]}")
if clipping_samples:
print(f" ! 클리핑 발생: {len(clipping_samples)}개")
self.issues.append(f"클리핑: {clipping_samples[:3]}")
if silent_samples:
print(f" ! 거의 무음: {len(silent_samples)}개")
self.issues.append(f"무음: {silent_samples[:3]}")
if not any([short_samples, long_samples, clipping_samples, silent_samples]):
print(" ✓ 오디오 품질 양호")
def check_text_quality(self):
"""텍스트 품질 확인"""
print("\n[3] 텍스트 품질 확인 중...")
empty_texts = self.df[self.df['text'].str.strip() == '']
short_texts = self.df[self.df['text'].str.len() < 5]
special_chars = self.df[self.df['text'].str.contains(r'[^\w\sㄱ-ㅎㅏ-ㅣ가-힣.,!?]', na=False)]
if len(empty_texts) > 0:
print(f" ✗ 빈 텍스트: {len(empty_texts)}개")
self.issues.append(f"빈 텍스트: {len(empty_texts)}개")
if len(short_texts) > 0:
print(f" ! 너무 짧은 텍스트 (<5자): {len(short_texts)}개")
if len(special_chars) > 0:
print(f" ! 특수문자 포함: {len(special_chars)}개")
if len(empty_texts) == 0:
print(" ✓ 텍스트 품질 양호")
def check_data_distribution(self):
"""데이터 분포 확인"""
print("\n[4] 데이터 분포 확인 중...")
# 텍스트 길이 분포
text_lengths = self.df['text'].str.len()
print(f" 텍스트 길이: 평균 {text_lengths.mean():.1f}자, "
f"중앙값 {text_lengths.median():.1f}자")
print(f" 최소 {text_lengths.min()}자, 최대 {text_lengths.max()}자")
# 전체 데이터셋 통계
print(f"\n 총 샘플 수: {len(self.df):,}개")
def generate_report(self):
"""최종 검증 리포트"""
print("\n" + "="*50)
print("=== 최종 검증 리포트 ===")
print("="*50)
if not self.issues:
print("\n✓✓✓ 모든 검사 통과! 데이터셋이 학습 준비 완료되었습니다.")
else:
print(f"\n발견된 문제: {len(self.issues)}개")
for i, issue in enumerate(self.issues, 1):
print(f" {i}. {issue}")
print("\n권장사항: 위 문제들을 수정한 후 학습을 시작하세요.")
# 사용 예시
checker = DatasetQualityChecker("./tts_dataset")
checker.check_all()
설명
이것이 하는 일: 위 코드는 완성된 TTS 데이터셋을 다각도로 자동 검증하여, 품질 문제를 빠르게 발견하고 리포트를 생성합니다. 첫 번째로, DatasetQualityChecker 클래스는 데이터셋 디렉토리와 메타데이터 파일을 입력받아 초기화됩니다.
check_all() 메서드는 5가지 주요 검증 단계를 순차적으로 실행하는 마스터 함수로, 각 단계에서 발견된 문제들을 self.issues 리스트에 누적합니다. 그 다음으로, 각 검증 함수가 특정 품질 측면을 점검합니다.
check_file_existence()는 메타데이터에 나열된 모든 오디오 파일이 실제로 존재하는지 확인합니다. 이것은 가장 기본적이지만 중요한 검사로, 학습 중 파일을 찾지 못해 크래시가 나는 것을 방지합니다.
check_audio_quality()는 각 오디오를 로드하여 지속 시간, 클리핑(과도한 음량), 무음 같은 기술적 문제를 탐지합니다. 클리핑이 있는 오디오는 왜곡된 소리가 나며, TTS 모델이 이상한 노이즈를 학습할 수 있습니다.
세 번째로, check_text_quality()는 텍스트 데이터의 문제를 찾습니다. 빈 텍스트, 너무 짧은 텍스트, 이상한 특수문자 등은 모두 학습을 방해하는 요소입니다.
check_data_distribution()은 데이터의 전체적인 균형을 평가합니다. 예를 들어, 모든 샘플이 2-3초 길이로 비슷하다면 다양성이 부족하고, 특정 문장 패턴만 반복된다면 모델이 과적합될 수 있습니다.
마지막으로, generate_report()는 모든 검사 결과를 요약하여 명확한 리포트를 출력합니다. 문제가 없으면 "학습 준비 완료" 메시지를, 문제가 있으면 구체적인 이슈 목록과 권장사항을 제공합니다.
이 리포트를 보고 어떤 부분을 수정해야 하는지 즉시 알 수 있습니다. 여러분이 이 코드를 사용하면 수동으로 몇 시간 걸릴 검증 작업을 몇 분 만에 완료할 수 있고, 놓치기 쉬운 문제들을 자동으로 발견할 수 있으며, 팀원들과 객관적인 품질 기준을 공유할 수 있습니다.
실무에서는 이 클래스를 CI/CD 파이프라인에 통합하여, 새 데이터가 추가될 때마다 자동으로 품질 검사를 실행합니다.
실전 팁
💡 첫 번째 검증에서 많은 문제가 발견되는 것은 정상입니다. 반복적으로 수정하고 검증하는 과정을 거쳐 품질을 높여가세요.
💡 데이터셋의 일부(예: 10%)를 먼저 검증하고, 패턴이 보이면 전체를 처리하기 전에 파이프라인을 수정하는 것이 효율적입니다.
💡 오디오 샘플링 레이트, 채널 수(모노/스테레오), 비트 깊이가 모두 일관되는지도 확인하세요. 일관성 없으면 학습 중 오류가 발생합니다.
💡 validation set을 사용해 실제 모델을 짧게 학습시켜보는 것도 좋은 검증 방법입니다. 10 epoch만 돌려봐도 데이터 문제를 빠르게 발견할 수 있습니다.
💡 데이터셋 버전 관리를 하세요. tts_dataset_v1, tts_dataset_v2 같은 식으로 버전을 나누고, 각 버전의 검증 리포트를 저장하면 나중에 문제 추적이 쉬워집니다.