본문으로 건너뛰기

07_음성분석 처리 과정 및 결과

· 약 7분
sbin
SceneMakerAI 팀

들어가며


음성을 글자 변환 (Speech to Text) 하는 과정을 구체적으로 명시한 글이다.

json 으로 정형화 하는 과정을 보여준다.

음성 분석 과정을 가장 쉽게 요약하면 아래와 같다.

  1. 잡음 제거 (Denoise)
  2. 사람 말하는 구간 탐색 (VAD)
  3. 화자 분리 (Pyannotate)
  4. 어떤 언어인지 탐지 (LID)
  5. 음성 인식 (ASR)

최적화를 위한 여러 라이브러리가 존재하는 것을 파악하였고, 최종 Flow를 아래와 같이 하였다.

# worker-prep_stt/main.py

run()
0) _load_audio 오디오 로드 및 잡음 제거 (denoise)
1) vad.detect 발화 구간 (Silero VAD)
├ 1b) speaker.diarize 화자 구분 (pyannote)
2) _classify_languages 구간별 언어 판정 (LID)
3) _transcribe_batched ◀── 여기가 진짜 음성→텍스트 (ASR)
4) _assemble_segments 단어→문장 재조립 + 화자 매핑
5) json 저장

1. 잡음 제거 (DeepFilterNet v3)

  • BGM/효과음/군중 잡음 제거 → ASR 환각 감소. 길이는 변하지 않음
DEEPFILTER_MODEL = "deepfilternet3"
ATTEN_LIM_DB = -30 # -30=음성 보존 잡음 감쇠 상한(dB). None=full power,
CHUNK_SEC = 30 # 긴 오디오 청크 길이 (DF 스펙트로그램 VRAM 한계)

## lib/audio/denoise.py

def process(audio_np: np.ndarray, sr: int) -> tuple[np.ndarray, int]:
...
audio = torch.from_numpy(audio_np)
if audio.ndim > 1:
audio = audio.mean(dim=1) # → mono
audio = audio.unsqueeze(0) # [1, T]
if sr != df_sr:
audio = F.resample(audio, sr, df_sr)

total = audio.shape[-1]
chunk = CHUNK_SEC * df_sr

# === 30초를 청크 시간 설정 뒤 진행 ===

if total <= chunk:
# 실제 잡음 제거 실행 함수 <- 이 부분에서 잡음 제거 진행
enhanced = enhance(model=_model, df_state=_df_state, audio=audio,
pad=True, atten_lim_db=ATTEN_LIM_DB)
else:
n = (total + chunk - 1) // chunk
log.info(f"denoise chunked: {total / df_sr:.1f}s → {n} chunks of {CHUNK_SEC}s")
parts = []
for s in range(0, total, chunk):
parts.append(enhance(
model=_model, df_state=_df_state, audio=audio[..., s:s + chunk],
pad=True, atten_lim_db=ATTEN_LIM_DB,
))
enhanced = torch.cat(parts, dim=-1)

return enhanced.squeeze(0).cpu().numpy().astype(np.float32), df_sr
  • DF (Deepfilternet) 가 원하는 sr (sampling rate) 48 k 이기에, 임시로 16k → 48k 변환.

    • 추후 호출 시 16k 다운 샘플 진행 필요

    • ASR (transcribe) 시에만 쓰임. LID , VAD 구간에서 쓰면 안됨.

  • 오디오가 길면 스펙트로그램이 커져서 GPU OOM 가능.

    • CNUNK_SEC = 30 설정 (한 조각의 길이가 최대 30초)

    • chunk = 30 x 48000 (30초를 샘플 수로 환산한 값)

    • 결론

      • 값을 키우면 조각당 처리량은 오르나, VRAM 이 위험하다

      • 값을 줄이면 조각당 초리량은 내리나, VRAM 이 안전하다

  • 예시 입출력

입력: [·♪♪·말♪♪♪·♪·말말·♪♪♪·] ← 잡음() 섞인 소리 배열
출력: [···말·····말말······] ← 잡음만 걷어낸 소리 배열

2. 발화 구간 추출 (VAD)

  • 사람이 말하는 부분만 추출
model="silero_vad"

# lib/audio/vad.py

def detect(audio_np: np.ndarray, sr: int = 16000) -> list[tuple[float, float]]:
"""발화 구간 타임스탬프 추출.

audio_np : float32 1D numpy (16kHz 권장)
sr : 샘플레이트 (Silero VAD 는 16k / 8k 만 공식 지원)

Returns: [(start_sec, end_sec), ...] — 발화 구간 (정렬됨)
"""

audio_t = torch.from_numpy(audio_np).float()
ts_list = _get_speech_timestamps(
audio_t, _model, sampling_rate=sr,
min_speech_duration_ms=int(MIN_SPEECH_S * 1000),
max_speech_duration_s=MAX_SPEECH_S,
min_silence_duration_ms=int(MIN_SILENCE_S * 1000),
)
return [(t["start"] / sr, t["end"] / sr) for t in ts_list]



  • 사람이 말하는 구간을 찾아내고, 샘플링 레이트를 16kHz 로 설정
  • 예시 입출력
입력: [침묵····말·······침묵··말말····침묵] ← 소리 배열 # np.ndarray (float32 1D)
출력: [(2.1,5.8), (40.3,47.2), ...] ← 말하는 구간 시각만 # list[tuple[float, float]]

3. 화자 분리

  • 영상에는 다양한 사람이 나오고, 그 사람들의 목소리는 당연히 다르다.
  • 이번엔 화자를 구분 시키는 부분 이다. 오픈소스 라이브러리. pyannotate 도입하였다.
PYANNOTE_DIARIZE = "pyannote-diarization" # 화자 구분 (community-1, self-contained)

# lib/audio/speaker.py

def diarize(audio_np: np.ndarray, sr: int = 16000) -> list[tuple[float, float, str]]:
"""화자 턴 타임라인 추출.

audio_np : float32 1D numpy (16k mono)
Returns : [(start_s, end_s, speaker_label), ...] (시간순)
"""
#...

waveform = torch.from_numpy(audio_np).unsqueeze(0).to(_device)
out = _pipeline({"waveform": waveform, "sample_rate": sr})
# pyannote 4.x: DiarizeOutput.speaker_diarization 이 Annotation
turns = [
(turn.start, turn.end, speaker)
for turn, _, speaker in out.speaker_diarization.itertracks(yield_label=True)
]
log.info(f"diarization: {len(turns)} turns, {len(set(t[2] for t in turns))} speakers")
return turns

추출 예시 결과

image

입출력 형태

입력: 오디오 전체, 16kHz Float 배열

출력: 화자 구분
[
(0.0, 5.8, "SPEAKER_00"), # 0~5.8초: 화자0
(5.8, 12.3, "SPEAKER_01"), # 5.8~12.3초: 화자1
(12.3, 15.0, "SPEAKER_00"), # 다시 화자0
(40.3, 47.2, "SPEAKER_02"), # 화자2
...
]

Whisper-large-v3는 OpenAI에서 공개한 최신 음성 인식 모델로,

다국어 음성 인식, 자동 자막 생성, 오디오 텍스트 변환 등의 작업에서 뛰어난 성능을 발휘하여, 도입하게 되었다.

4. 언어 판정 (LID)

  • 어떤 언어인지를 식별하는 과정이다.
MODEL : "whisper-large-v3"
COMPUTE_TYPE : "float16" # GPU 서버이기에 COMPUTE_TYPE 은 float16 선정

# lib/audio/whisper.py

def detect_language(chunk: np.ndarray) -> tuple[str, float]:
"""raw chunk → (lang_code, prob). LID 는 raw 오디오에서 (PoC Strategy 2)."""

lang_code, prob, _all_probs = _model.detect_language(chunk)
return lang_code, prob

  • wav 음성 파일 LID 과정.

    • ko=1.00 → 가장 높은 확률로 ko 로 인식한다는 뜻이다.
  • 두 모델 (Whisper LID / VoxLingua107 LID) 비교 하였고, PoC 뒤 Whisper 선정

2026-05-28 18:35:21 [INFO] lid_bench: DeepFilterNet3 loaded (sr=48000, device=cuda:0)
2026-05-28 18:35:21 [INFO] lid_bench: Loading Silero VAD...
2026-05-28 18:35:21 [INFO] lid_bench: Loading Whisper LID: mobiuslabsgmbh/faster-whisper-large-v3-turbo
2026-05-28 18:35:22 [INFO] lid_bench: Loading VoxLingua107 LID (savedir=/stg/models/voxlingua107)
2026-05-28 18:35:22 [INFO] lid_bench: === models loaded ===
2026-05-28 18:35:22 [INFO] lid_bench: === bench start: /stg/vod/scenemaker/sound_full/docu.wav ===
2026-05-28 18:35:23 [INFO] lid_bench: denoise chunked: 3520.7s → 118 chunks of 30s
2026-05-28 18:35:33 [INFO] lid_bench: denoise saved: output/denoise/docu.wav
2026-05-28 18:35:43 [INFO] lid_bench: audio 3520.7s → VAD 383 speech segments
2026-05-28 18:35:44 [INFO] lid_bench: [ 11.5~ 12.7s] W-raw=ko=1.00 V-raw=ko=0.99 W-den=ko=1.00 V-den=ko=0.80
2026-05-28 18:35:44 [INFO] lid_bench: [ 13.1~ 15.3s] W-raw=ko=1.00 V-raw=ko=1.00 W-den=ko=1.00 V-den=ko=1.00
...
026-05-28 18:35:45 [INFO] lid_bench: [ 278.4~ 281.1s] W-raw=ko=1.00 V-raw=ko=1.00 W-den=ko=1.00 V-den=ko=1.00
2026-05-28 18:35:45 [INFO] lid_bench: [ 282.6~ 284.3s] W-raw=en=0.30 V-raw=it=0.79 W-den=en=0.46 V-den=pt=0.32

5. 음성 인식 (ASR)

MODEL : "whisper-large-v3"
COMPUTE_TYPE : "float16"
BATCH_SIZE=16

# lib/audio/whisper.py


def transcribe_batched(audio: np.ndarray, language: str) -> list[dict]:
"""오디오를 지정 언어로 배치 transcribe (내부 VAD 로 발화 구간만, 30s 윈도우 병렬).

audio 는 보통 "해당 언어 외 구간을 묵음 처리한 전체 길이 스트림" → 내부 VAD 가
묵음을 건너뛰므로 그 언어 발화만 인식되고, timestamp 는 원본 시각 그대로.

word_timestamps 로 단어 단위 반환 → 호출측(stt_service)이 화자 턴/문장부호 기준으로
재분할 (배치는 segment 가 30s 로 뭉치므로 세밀도/화자 정확도 복원).

Returns: [{"start", "end", "word", "seg_logprob"}, ...] (단어 단위, 절대 시각)
seg_logprob = 그 단어가 속한 segment 의 avg_logprob (환각 필터용)
"""
if _model is None:
load_model()
segments_gen, _info = _batched.transcribe(
audio,
language=language,
batch_size=BATCH_SIZE, # 동시에 GPU 올리는 3-s 윈도우 수, 16 으로 설정함
beam_size=5,
no_speech_threshold=0.6,
log_prob_threshold=-1.0,
compression_ratio_threshold=2.4,
condition_on_previous_text=False,
repetition_penalty=1.2,
no_repeat_ngram_size=3,
vad_filter=True, # 묵음 구간 건너뛰기 (묵음 스트림 처리의 핵심)
word_timestamps=True, # 단어 단위 타임스탬프
)
words = []
for s in segments_gen:
for w in (s.words or []):
words.append({
"start": float(w.start), "end": float(w.end),
"word": w.word, "seg_logprob": s.avg_logprob,
})
return words
  • 동작 과정 기록만 되어 있는 상태.
2026/06/25 17:15:31 INFO [stt_service.py:_transcribe_batched:204] - batched [zh]: 81 words from 7 ranges
2026/06/25 17:15:31 INFO [transcribe.py:transcribe:390] - Processing audio with duration 58:40.749
2026/06/25 17:15:34 INFO [transcribe.py:transcribe:458] - VAD filter removed 58:38.445 of audio
2026/06/25 17:15:34 INFO [stt_service.py:_transcribe_batched:204] - batched [vi]: 11 words from 1 ranges
2026/06/25 17:15:34 INFO [stt_service.py:_assemble_segments:252] - assemble: 314 segments from 2477 words
2026/06/25 17:15:34 INFO [util.py:write_json:26] - wrote json → output/2/result.json

...
  • json 으로 결과 확인 가능하다.
{
"idx": 0,
"start": "00:00:07.2",
"end": "00:00:09.9",
"text": "스포일러 엄청 커 저번이랑",
"lang": "ko",
"speaker": "S003"
},

마무리

목표였던 STT 및 정형화 과정이 완료되었다.

추출 내용 결과, 자막에 문제가 있는 것을 확인되었다.

추후 재보정 (refine) 진행할 예정이다.

참고 링크)

본 글은 과학기술정보통신부·정보통신산업진흥원 「2026년 오픈소스 AI·SW 개발·활용 지원사업」의 지원으로 수행된 연구 결과입니다.