파이썬 한국어 실시간 음성인식 무료 구현 - faster-whisper + Silero VAD

마이크에 대고 말하면 발화가 끝나는 시점을 자동으로 감지해 한국어 텍스트로 출력하는 실시간 음성인식 프로그램을 파이썬으로 단계별로 만들어봅니다. faster-whisper와 Silero VAD를 사용하며, 가입·로그인·API 키·사용료가 전부 필요 없습니다.

.

faster-whisper는 OpenAI가 MIT 라이선스로 공개한 Whisper 모델을 CTranslate2로 최적화한 구현체입니다. 최초 실행 시 모델 파일이 자동 다운로드되며(계정 불필요), 이후에는 오프라인에서도 동작합니다.

.

각 단계마다 정상 동작인지 확인하고 다음 단계로 넘어갑니다. 문제 발생시 다음 단계로 넘어가지 말고 트러블슈팅 섹션을 확인해보세요. 문제가 생겼을 때 어느 단계까지는 정상인지 알 수 있도록 파이프라인을 쪼개서 확인하는 것이 이 글의 핵심 구성입니다.

.

본 글의 내용은 클로드로 사전 조사 후  맥북(macOS Apple Silicon)에서 검증 및 수정했습니다. 

.

0단계. 가상환경 준비

미니콘다가 필수는 아니지만(venv 등 다른 가상환경을 사용해도 됩니다), 본 포스팅에서는 미니콘다가 설치된 상태에서 진행합니다. 미니콘다 설치 방법은 아래 포스팅을 참고하세요.

맥북(Apple Silicon)에 미니콘다 + 파이토치 설치하기 (기존 설치 완전 삭제 포함)

.

이제 파이썬 가상환경을 생성하고 활성화합니다. 이 글의 내용은 Python 3.11에서 확인했습니다.

conda create -n stt python=3.11 -y
conda activate stt

.

1단계. 패키지 설치

필요한 패키지를 설치합니다. 

pip install faster-whisper silero-vad sounddevice numpy

.

macOS에서 sounddevice는 시스템의 CoreAudio를 사용하므로 별도 오디오 라이브러리 설치가 필요 없습니다. (Ubuntu에서는 sudo apt install libportaudio2가 먼저 필요합니다.)

.

검증 — 아래를 실행해서 세 패키지가 모두 임포트되고 VAD 모델이 로드되는지 확인합니다.

python -c "
import sounddevice as sd
import faster_whisper
from silero_vad import load_silero_vad
print('faster-whisper:', faster_whisper.__version__)
model = load_silero_vad()
print('Silero VAD 로드 성공')
print('기본 입력 장치:', sd.query_devices(kind='input')['name'])
"

.

제 맥북에서는 다음처럼 출력되었습니다. 세 줄이 모두 나오면 통과입니다. Silero VAD의 모델 파일은 pip 패키지 안에 포함되어 있어서 이 단계에서는 인터넷 연결도 필요하지 않습니다.

faster-whisper: 1.2.1

Silero VAD 로드 성공

기본 입력 장치: MacBook Air 마이크

.

2단계. 마이크 캡처 확인

우선 마이크로 소리가 제대로 들어오는지부터 확인합니다. 음성인식이 안 될 때 원인 파악하기 쉽도록  마이크로 음성이 잘 들어오는지 검증해두는 것이 좋습니다. 

다음 코드를 step2_mic_test.py 이름으로 저장하여 사용했습니다.

import numpy as np
import sounddevice as sd

SAMPLE_RATE = 16000  # 음성인식 표준. 사람 음성 대역(8kHz 이하)의 2배로 충분

print("3초간 녹음합니다. 마이크에 대고 말해보세요...")
audio = sd.rec(
    int(3 * SAMPLE_RATE), samplerate=SAMPLE_RATE, channels=1, dtype="float32"
)
sd.wait()
audio = audio.flatten()

rms = np.sqrt(np.mean(audio**2))
peak = np.abs(audio).max()
print(f"RMS 레벨: {rms:.4f}")
print(f"피크 레벨: {peak:.4f}")

if peak < 0.01:
    print("판정: 소리가 거의 안 들어왔습니다. 마이크 권한/장치를 확인하세요.")
elif peak > 0.99:
    print("판정: 입력이 클리핑됐습니다. 입력 볼륨을 낮추세요.")
else:
    print("판정: 마이크 입력이 들어오고 있습니다.")
    print("(가만히 있어도 주변 소음 때문에 이 판정이 나올 수 있습니다.")
    print(" 핵심은 말할 때와 안 할 때의 피크 레벨 차이입니다.)")

.

테스트 코드를 실행해봅니다.

python step2_mic_test.py

.

macOS에서 처음 실행하면 터미널(또는 IDE)에 마이크 접근 권한을 묻는 팝업이 뜹니다. 팝업이 안 뜨고 피크가 0에 가깝다면 시스템 설정 → 개인정보 보호 및 보안 → 마이크에서 사용 중인 터미널 앱을 허용하세요.

.

말하면서 실행했을 때: 피크 레벨이 대략 0.05~0.9 사이면 정상입니다.

.

가만히 있으면서 실행했을 때: 피크 레벨이 말할 때보다 확연히 낮아야 합니다.

.

주의: 무음 상태에서도 주변 소음이 잡히기 때문에 "마이크 입력이 들어오고 있습니다" 판정이 나오는 것이 정상입니다(실측 예: 무음 시 피크 0.017). 이 단계에서 확인하려는 것은 절대값이 아니라 말할 때와 안 할 때의 레벨 차이입니다. 두 경우의 피크가 거의 같다면 마이크가 목소리를 못 잡고 있다는 뜻입니다.

.

3단계. faster-whisper 인식 확인 (파일 기반)

실시간으로 가기 전에, 녹음된 오디오 한 덩어리가 텍스트로 잘 변환되는지 확인합니다.

다음 코드를 step3_whisper_test.py 이름으로 저장하여 사용했습니다.

import sounddevice as sd
import numpy as np
from faster_whisper import WhisperModel

SAMPLE_RATE = 16000

print("모델 로딩 중... (최초 실행 시 다운로드로 수 분 소요)")
model = WhisperModel("small", device="cpu", compute_type="int8")

print("5초간 녹음합니다. 아무 문장이나 또박또박 말해보세요...")
audio = sd.rec(int(5 * SAMPLE_RATE), samplerate=SAMPLE_RATE,
              channels=1, dtype="float32")
sd.wait()
audio = audio.flatten()

print("인식 중...")
segments, info = model.transcribe(audio, language="ko")
for seg in segments:
    print(f"[{seg.start:.1f}s - {seg.end:.1f}s] {seg.text}")

몇 가지 설정의 의미:

  • "small": 모델 크기. 한국어는 tiny/base에서 정확도 하락이 커서 small 이상을 권장합니다. 최초 실행 시 약 500MB가 자동 다운로드됩니다(로그인 불필요, ~/.cache/huggingface에 저장).
  • device="cpu", compute_type="int8": Apple Silicon에서는 CPU + int8 양자화 조합이 무난합니다. M1 이상이면 small 모델 기준 실시간보다 빠르게 처리됩니다.
  • language="ko": 언어를 고정하면 자동 감지 과정이 생략되어 속도와 정확도 모두 유리합니다.

.

"오늘 날씨가 참 좋습니다"를 말하여 출력된 텍스트가 말한 내용과 일치하는지 확인합니다. 조사나 띄어쓰기가 조금 다른 것은 정상입니다.

  1. 인식에 걸린 시간이 녹음 길이(5초)보다 짧은지 체감으로 확인합니다. 훨씬 오래 걸린다면 트러블슈팅을 확인하세요.
  2. 이 단계를 통과하면 "마이크 → 모델" 경로 전체가 정상이라는 뜻입니다.

.

4단계. Silero VAD 동작 확인

실시간 인식의 핵심은 "말이 언제 시작되고 끝났는지"를 판단하는 VAD입니다. 통합 전에 VAD 단독으로 검증합니다.

다음 코드를 step4_vad_test.py 이름으로 저장하여 사용했습니다.

import sounddevice as sd
import numpy as np
import torch
from silero_vad import load_silero_vad

SAMPLE_RATE = 16000
CHUNK = 512  # Silero VAD는 16kHz에서 512 샘플(32ms) 단위 입력이 필수

vad = load_silero_vad()

print("10초간 마이크를 감시합니다. 말할 때와 안 할 때를 번갈아 보세요.")
print("막대가 길수록 '음성일 확률'이 높다는 뜻입니다.\n")

def callback(indata, frames, t, status):
    chunk = torch.from_numpy(indata[:, 0].copy())
    prob = vad(chunk, SAMPLE_RATE).item()
    bar = "#" * int(prob * 40)
    print(f"{prob:.2f} |{bar}")

with sd.InputStream(samplerate=SAMPLE_RATE, channels=1,
                    dtype="float32", blocksize=CHUNK,
                    callback=callback):
    sd.sleep(10_000)

.

실행한 후, 아무 소리를 안 내거나 책상을 두들기는 소리에는 0.1 이하 정도 값을 유지해야 하며 목소리에만 0.7 이상의 값을 출력해야 합니다.

Silero VAD가 에너지 기반 방식보다 좋은 이유가 이 부분으로, "큰 소리"가 아니라 "사람 음성"에만 반응합니다. 

.

5단계. 전체 파이프라인 통합 — 실시간 음성인식

이제 지금까지 구현한 것을 하나로 연결합니다. 구조는 다음과 같습니다.

마이크 (512샘플씩 도착)

  → VAD로 매 청크의 음성 확률 계산

  → 발화 시작 감지 시 버퍼에 누적

  → 500ms 이상 무음이면 발화 종료로 판단

  → 누적된 발화 구간을 Whisper에 전달 (별도 스레드)

  → 텍스트 출력

.

음성 인식을 별도 스레드로 분리하는 이유는, 마이크 콜백 안에서 음성 인식을 돌리면 음성 인식하는 동안 오디오 버퍼가 넘쳐 소리가 끊기기 때문입니다.

다음 코드를 step5_realtime_stt.py 이름의 파일로 저장하여 사용합니다. 

import queue
import threading
import numpy as np
import sounddevice as sd
import torch
from faster_whisper import WhisperModel
from silero_vad import load_silero_vad

SAMPLE_RATE = 16000
CHUNK = 512                 # 32ms
SPEECH_THRESHOLD = 0.5      # 이 확률 이상이면 음성으로 판단
SILENCE_MS = 500            # 이 시간 이상 무음이면 발화 종료
SILENCE_CHUNKS = SILENCE_MS // 32
MIN_SPEECH_CHUNKS = 8       # 너무 짧은 소리(기침 등)는 무시

print("모델 로딩 중...")
vad = load_silero_vad()
whisper = WhisperModel("small", device="cpu", compute_type="int8")
print("준비 완료. 말씀하세요. (Ctrl+C로 종료)\n")

speech_queue = queue.Queue()

def transcribe_worker():
    while True:
        audio = speech_queue.get()
        segments, _ = whisper.transcribe(audio, language="ko")
        text = "".join(seg.text for seg in segments).strip()
        if text:
            print(f">> {text}")

threading.Thread(target=transcribe_worker, daemon=True).start()

buffer = []
speech_chunks = 0
silence_count = 0
speaking = False

def callback(indata, frames, t, status):
    global buffer, speech_chunks, silence_count, speaking
    samples = indata[:, 0].copy()
    prob = vad(torch.from_numpy(samples), SAMPLE_RATE).item()

    if prob >= SPEECH_THRESHOLD:
        if not speaking:
            speaking = True
            print("(발화 감지)")
        buffer.append(samples)
        speech_chunks += 1   # 버퍼에는 무음도 섞이므로 음성 청크만 따로 센다
        silence_count = 0
    elif speaking:
        buffer.append(samples)  # 어미가 잘리지 않게 무음도 잠시 포함
        silence_count += 1
        if silence_count >= SILENCE_CHUNKS:
            if speech_chunks >= MIN_SPEECH_CHUNKS:
                speech_queue.put(np.concatenate(buffer))
            buffer = []
            speech_chunks = 0
            silence_count = 0
            speaking = False
            vad.reset_states()  # VAD는 내부 상태를 유지하므로 발화마다 초기화

with sd.InputStream(samplerate=SAMPLE_RATE, channels=1,
                    dtype="float32", blocksize=CHUNK,
                    callback=callback):
    while True:
        sd.sleep(1000)

.

다음 네 가지 시나리오를 순서대로 테스트하여 검증합니다. 

  1. 정상 인식: "안녕하세요, 음성인식 테스트입니다"라고 말하고 멈추면, 약 0.5초 뒤 (발화 감지)에 이어 인식된 텍스트가 출력되어야 합니다.
  2. 무음 환각 방지: 30초간 아무 말도 하지 않았을 때 아무 텍스트도 출력되지 않아야 합니다. VAD 없이 Whisper만 쓰면 이 상황에서 "시청해주셔서 감사합니다" 같은 환각 텍스트가 나오는데, 그것이 차단되는지 확인하는 것입니다.
  3. 문장 분리: 한 문장을 말하고 1초 쉰 뒤 다른 문장을 말하면, 두 개의 별도 결과로 출력되어야 합니다.
  4. 연속 발화 유지: 문장 중간에 아주 짧게(0.3초 이하) 숨을 고르는 정도로는 문장이 잘리지 않아야 합니다. 자주 잘린다면 SILENCE_MS를 700~1000으로 올려보세요.

.

트러블슈팅

pip 설치는 됐는데 sounddevice 임포트에서 PortAudio 에러 (Ubuntu) sudo apt install libportaudio2 후 재시도. macOS에서는 발생하지 않습니다.

2단계에서 피크 레벨이 계속 0에 가까움 (macOS) 시스템 설정 → 개인정보 보호 및 보안 → 마이크에서 터미널/IDE 허용 여부 확인. 허용 후에는 터미널을 완전히 종료했다가 다시 실행해야 반영됩니다.

3단계 모델 다운로드가 안 됨 faster-whisper는 최초 실행 시 Hugging Face 서버에서 모델을 받습니다. 로그인은 필요 없지만 인터넷 연결은 필요합니다. 회사망 등에서 차단된 경우, 다른 네트워크에서 한 번 받아두면 ~/.cache/huggingface에 캐시되어 이후 오프라인 동작합니다.

3단계 인식이 너무 느림 compute_type="int8"인지 확인하고, 그래도 느리면 모델을 "base"로 낮춰봅니다. 반대로 여유가 있으면 "medium"으로 올리면 정확도가 향상됩니다. NVIDIA GPU가 있는 환경(Ubuntu + RTX 등)이라면 device="cuda"로 크게 빨라집니다.

4단계에서 VAD 확률 계산 중 크기 에러 Silero VAD는 16kHz 기준 정확히 512 샘플 단위 입력만 받습니다. blocksize=512가 다른 값으로 바뀌지 않았는지 확인하세요.

5단계에서 문장 앞부분이 잘림 발화 시작 판정 직전의 오디오가 버려져서 첫 음절이 잘리는 경우입니다. 최근 몇 청크를 항상 링 버퍼로 들고 있다가 발화 시작 시 함께 붙이는 "프리롤(pre-roll)" 처리를 추가하면 해결됩니다. AI에게는 "발화 시작 감지 시 직전 300ms를 프리롤로 포함해줘"라고 요청하면 됩니다.

.

관련 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다