안녕하세요 오늘은
Python을 이용해서 음성 데이터 QC(품질 관리, Quality Control ) 리포트를 만드는 방법을 소개하려고 합니다.
음성 데이터를 TTS(Text-to-Speech)나 ASR(Automatic Speech Recognition) 같은 모델 학습에 활용하려면, 데이터 품질 관리가 필수적입니다.
하지만 수천 개에 달하는 오디오 파일을 사람이 직접 일일이 들어보면서 확인한다는 건 사실상 불가능합니다.
그래서 오늘은 Python과 오디오 라이브러리(librosa, soundfile, pandas 등)를 활용해, 자동으로 품질 지표를 추출하고 CSV 리포트로 정리하는 방법을 살펴보겠습니다. 이 방법을 사용하면 대규모 데이터셋도 훨씬 효율적으로 관리할 수 있습니다.
즉, 음성 데이터에서의 QC 란 녹음 파일이 샘플레이트, 채널, 무음 길이, 볼륨, 잡음 기준을 만족하는지 검사하는 것을 의미합니다.
QC 란? Quality Control
- 품질 관리라는 의미를 가지고 있으며
- 원래는 제조업, 소프트웨어 공학, 서비스 산업 등에서 제품이나 서비스가 정해진 품질 기준을 만족하는지 검사·관리하는 과정을 뜻하는 용어입니다.
- 이 게시글에서는 음성 데이터셋이 학습에 적합한 품질을 갖췄는지 자동으로 검사하고 리포트 만드는 과정을 의미합니다.
TTS/ASR 학습 데이터를 모으다 보면 의외로 다양한 문제가 생깁니다.
- 녹음 버튼을 일찍 눌러 생긴 앞뒤의 긴 무음
- 녹음 환경이나 장치에 따라 다른 샘플레이트, 채널 불일치 (예: 스테레오 vs 모노, 44.1kHz vs 32kHz)
- 파일마다 제각각인 RMS 음량 레벨 (너무 작은 소리 혹은 지나치게 큰 소리)
- 녹음 과정에서 생긴 클리핑(0dBFS 초과)
- 주변 환경 소음 때문에 올라간 노이즈 플로어
- 발화 도중 불필요하게 긴 침묵 구간
이런 문제들은 학습 데이터의 일관성을 해치고, 결국 모델 성능에도 악영향을 줍니다.
따라서 음성 데이터셋을 준비할 때는 자동으로 품질 지표를 뽑아내고, OK/Check 상태를 표시해주는 QC 리포트가 큰 도움이 됩니다.
이번 글에서는 이런 리포트를 Python으로 손쉽게 만들어보는 과정을 다루겠습니다.
1. preprocess.py
import argparse
from pathlib import Path
import sys
import numpy as np
import soundfile as sf
import pandas as pd
import librosa
from scipy.signal import butter, filtfilt
import datetime
EPS = 1e-12
def run_qc(input_dir: Path,
target_sr: int = 32000,
frame_ms: int = 10,
top_db: int = 45,
max_leadtrail_ms: int = 500,
rms_range_dbfs=(-28, -16),
noise_floor_max_dbfs: float = -45.0,
# csv_name: str = "qc_report.csv",
# 내부 무음 통계 옵션
internal_top_db: int = 40,
internal_gap_min_ms: int = 800):
rows = []
files = sorted(list(input_dir.glob("*.wav")))
if not files:
print("⚠️ 입력 폴더에 .wav 파일이 없습니다.")
return
for p in files:
try:
y, sr = sf.read(p)
if y.ndim == 2:
ch = y.shape[1]
y = y.mean(axis=1)
else:
ch = 1
dur = len(y) / sr
peak = float(np.max(np.abs(y)))
rms = float(np.sqrt(np.mean(y ** 2)))
frames_db, _ = compute_frame_db(y, sr, frame_ms)
floor_db = estimate_noise_floor(frames_db, pct=0.1)
lead_ms, trail_ms, _ = detect_lead_trail_ms(y, sr, frame_ms, top_db)
# ✅ 내부 무음 통계 (min_gap_ms 이상만 집계)
int_cnt, int_max, int_mean, int_first3 = internal_silence_stats(
y, sr, top_db=internal_top_db, min_gap_ms=internal_gap_min_ms
)
issues = []
if ch != 1: issues.append("not_mono")
if sr != target_sr: issues.append("sr_mismatch")
if peak >= 0.999: issues.append("clipping")
if not (rms_range_dbfs[0] <= dbfs(rms) <= rms_range_dbfs[1]): issues.append("rms_out")
if floor_db > noise_floor_max_dbfs: issues.append("noisy_floor")
if lead_ms > max_leadtrail_ms or trail_ms > max_leadtrail_ms: issues.append("long_silence")
# 내부 무음은 이번엔 '이슈'로 치지 않고, 참고용 열만 추가 (원하면 여기에 플래그 추가 가능)
rows.append(dict(
filename=p.name,
duration_sec=round(dur, 3),
samplerate=sr,
channels=ch,
peak_dbfs=round(dbfs(peak), 2),
rms_dbfs=round(dbfs(rms), 2),
noise_floor_dbfs=round(floor_db, 2),
leading_ms=lead_ms,
trailing_ms=trail_ms,
# ✅ 내부 무음 분포 열
internal_silence_count=int_cnt,
internal_silence_max_ms=(None if int_max is None else int(int_max)),
internal_silence_mean_ms=(None if int_mean is None else round(float(int_mean), 1)),
internal_silence_first3_ms=("|".join(map(lambda x: str(x), int_first3)) if int_first3 else ""),
status="OK" if not issues else "CHECK",
issues=";".join(issues)
))
except Exception as e:
rows.append(dict(filename=p.name, status="ERROR", issues=str(e)))
df = pd.DataFrame(rows).sort_values("filename")
# ✅ 오늘 날짜 붙여서 파일명 생성
today = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
out = input_dir.parent / f"qc_report_{today}.csv"
df.to_csv(out, index=False, encoding="utf-8-sig")
print(f"✅ QC 저장: {out}")
print(df.head(min(10, len(df))))
def main():
args = parse_args()
run_qc(args.input)
if __name__ == "__main__":
main()
QC 파일에 포함된 요소 (컬럼값 목록)
- Duration: 파일 길이(초)
- Samplerate: 표본화 주파수 (예: 32000Hz)
- Channels: 모노/스테레오 여부
- Peak dBFS: 최대 음압, 클리핑 여부 확인
- RMS dBFS: 평균 볼륨 수준
- Noise floor: 무음 구간 잡음 레벨
- Leading/Trailing Silence: 앞뒤 무음 길이 (ms)
- Internal Silence: 문장 중간에 0.8초 이상 뜨는 긴 무음 통계
- Issues: 문제 있으면 자동 플래그 (예: clipping; sr_mismatch; noisy_floor)
아래와 같은 파일이 생성된 것을 볼 수 있습니다.
issues 내용에 따르면 목표 샘플레이트는 32khz 인것에 비해 대상 데이터의 샘플레이트는 48khz 이기 때문에 sr_mismatch 태그가 모든 데이터에 붙은 것을 알 수 있습니다. 또한 50개 중 5개의 데이터는 중간 공백이 800ms 이상이기 때문에 long_silence 태그가 붙은 것을 알 수 있습니다.

이렇게 수많은 데이터를 직접 들어보지 않고도 리포트 작성을 통해 기준에 어긋나는 데이터를 골라낼 수 있습니다.
감사합니다.
'Python' 카테고리의 다른 글
| [Python] PyTorch 텐서 차원 다루기 (0) | 2025.08.26 |
|---|---|
| [Python] gRPC로 양방향 통신하기 (0) | 2025.08.22 |
| [Python] Pytorch에서 벡터 합치기 (임베딩 결합 방식) (0) | 2025.08.19 |
| [Python] JSONL 포맷으로 음성 데이터셋 정리하기 (1) | 2025.08.18 |
| [Python] librosa로 WAV 파일 무음 제거하기 (1) | 2025.08.17 |