마이크에 대고 말하면 발화가 끝나는 시점을 자동으로 감지해 한국어 텍스트로 출력하는 실시간 음성인식 프로그램을 파이썬으로 단계별로 만들어봅니다. 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": 언어를 고정하면 자동 감지 과정이 생략되어 속도와 정확도 모두 유리합니다.
.
"오늘 날씨가 참 좋습니다"를 말하여 출력된 텍스트가 말한 내용과 일치하는지 확인합니다. 조사나 띄어쓰기가 조금 다른 것은 정상입니다.
- 인식에 걸린 시간이 녹음 길이(5초)보다 짧은지 체감으로 확인합니다. 훨씬 오래 걸린다면 트러블슈팅을 확인하세요.
- 이 단계를 통과하면 "마이크 → 모델" 경로 전체가 정상이라는 뜻입니다.

.
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) |
.
다음 네 가지 시나리오를 순서대로 테스트하여 검증합니다.
- 정상 인식: "안녕하세요, 음성인식 테스트입니다"라고 말하고 멈추면, 약 0.5초 뒤 (발화 감지)에 이어 인식된 텍스트가 출력되어야 합니다.
- 무음 환각 방지: 30초간 아무 말도 하지 않았을 때 아무 텍스트도 출력되지 않아야 합니다. VAD 없이 Whisper만 쓰면 이 상황에서 "시청해주셔서 감사합니다" 같은 환각 텍스트가 나오는데, 그것이 차단되는지 확인하는 것입니다.
- 문장 분리: 한 문장을 말하고 1초 쉰 뒤 다른 문장을 말하면, 두 개의 별도 결과로 출력되어야 합니다.
- 연속 발화 유지: 문장 중간에 아주 짧게(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를 프리롤로 포함해줘"라고 요청하면 됩니다.
.