영상에서 텍스트를 뽑고 싶었다 — ffmpeg, whisper-cpp, VAD로 로컬 STT 파이프라인 만들기
영상 파일이 하나 있고, 거기서 음성을 텍스트로 뽑아야 하는 상황이었습니다.
클라우드 STT API도 있습니다. 그중 OpenAI Whisper API는 월 $1.5 수준으로 저렴하고 정확도도 높습니다. 다만 매번 영상 파일을 외부 서버로 보내야 하고, 네트워크가 필요합니다. 반복적으로 쓸 거라면, 같은 Whisper 모델을 로컬에서 돌리는 파이프라인을 한 번 만들어두는 게 낫겠다는 생각이 들었습니다.
완성된 흐름은 이렇습니다.
영상 파일(.MOV) → ffmpeg(오디오 추출) → whisper-cpp(음성→텍스트) → 텍스트 출력
단순해 보이지만, 각 단계에서 "왜 이렇게 하는가"를 이해하면 꽤 흥미로운 지점들이 있습니다. ffmpeg가 오디오를 뽑을 때 왜 하필 16kHz 모노 PCM인지, whisper-cpp가 뭐고 왜 Python 버전 대신 C++ 포팅을 쓰는지, VAD라는 게 왜 필요한지 — 하나씩 따라가보겠습니다.
ffmpeg — 영상에서 오디오를 추출하는 도구
ffmpeg는 영상, 오디오, 이미지를 변환하는 멀티미디어 스위스 아미 나이프입니다. 1999년에 시작된 오픈소스 프로젝트로, 거의 모든 미디어 포맷을 다룰 수 있습니다. YouTube, Netflix, VLC 등 수많은 서비스와 도구가 내부적으로 ffmpeg를 사용합니다.
여기서는 딱 하나의 역할만 합니다. 영상 파일에서 오디오 트랙만 뽑아서, whisper가 읽을 수 있는 형식으로 변환하는 것입니다.
ffmpeg -i input.MOV -ar 16000 -ac 1 -c:a pcm_s16le output.wav -y
옵션이 네 개인데, 각각 이유가 있습니다.
-ar 16000: 샘플레이트 16kHz
소리를 디지털로 기록하려면 아날로그 파형을 일정 간격으로 "찍어서" 숫자로 저장해야 합니다. 이 찍는 빈도가 샘플레이트입니다. 16,000Hz는 초당 16,000번 찍는다는 뜻입니다.
음악 파일은 보통 44,100Hz(CD 품질)나 48,000Hz입니다. 그런데 왜 STT에서는 16,000Hz로 낮추는 걸까요?
나이퀴스트 정리(Nyquist theorem)에 의하면, 샘플레이트의 절반까지의 주파수를 재현할 수 있습니다. 16kHz 샘플레이트는 최대 8kHz까지의 소리를 담을 수 있다는 의미입니다.
사람 음성의 기본 주파수(fundamental frequency)는 남성 85~180Hz, 여성 165~255Hz 정도입니다. 하지만 발음을 구별하는 데 중요한 배음(harmonics)과 자음의 마찰음은 4~6kHz까지 분포합니다. 8kHz면 음성 인식에 필요한 정보를 충분히 담을 수 있습니다.
44.1kHz로 넣으면 어떻게 될까요? 동작은 합니다. 하지만 Whisper 모델 자체가 16kHz 오디오로 훈련되었기 때문에, 내부적으로 리샘플링이 일어납니다. 미리 16kHz로 맞춰서 넣는 게 불필요한 처리를 줄이고, 파일 크기도 약 1/3로 줄입니다.
-ac 1: 모노 채널
스테레오(2채널)는 좌우 스피커에 다른 소리를 보내서 공간감을 만듭니다. 음악 감상에는 의미가 있지만, 음성 인식에서는 "누가 뭐라고 말했는가"만 중요합니다. 공간 정보는 불필요합니다.
모노로 변환하면 데이터가 절반으로 줄고, 모델이 처리할 정보도 줄어듭니다.
-c:a pcm_s16le: 16비트 PCM
오디오 코덱을 지정하는 옵션입니다. pcm_s16le를 풀어보면:
- PCM (Pulse Code Modulation): 압축하지 않은 원시 오디오 데이터
- s16: signed 16-bit. 각 샘플을 -32,768 ~ +32,767 범위의 정수로 표현
- le: little-endian. 바이트 저장 순서
MP3나 AAC 같은 압축 포맷은 사람이 잘 못 듣는 부분을 버려서 파일 크기를 줄입니다. 음악 감상에서는 눈치채기 어렵지만, STT 모델은 그 "버려진 부분"에서 정보를 얻을 수도 있습니다. 비압축 PCM은 원본 정보를 그대로 유지합니다.
16비트는 96dB의 다이내믹 레인지를 제공합니다. 조용한 속삭임부터 큰 목소리까지 충분히 표현할 수 있는 범위입니다. 24비트나 32비트로 올려봐야 STT 정확도에 차이는 없고 파일만 커집니다.
-y: 덮어쓰기
출력 파일이 이미 있으면 묻지 않고 덮어씁니다. 자동화 파이프라인에서는 "사용자 확인 대기"가 전체 흐름을 멈추므로 필요한 옵션입니다.
변환 결과
iPhone으로 촬영한 3분짜리 .MOV 파일을 변환하면 이런 결과가 나옵니다.
# 원본: 3분 .MOV (약 500MB, H.264 영상 + AAC 48kHz 스테레오)
# 변환 후: WAV (약 5.5MB, PCM 16kHz 모노)
# 변환 시간: 1초 미만
500MB 영상에서 5.5MB WAV가 나옵니다. 영상 트랙을 버리고, 샘플레이트를 1/3로 낮추고, 스테레오를 모노로 바꿨으니 이 정도 차이가 납니다.
whisper-cpp — 로컬에서 도는 음성 인식 엔진
Whisper가 뭔가
Whisper는 OpenAI가 2022년 9월에 공개한 범용 음성 인식 모델입니다. 인터넷에서 수집한 68만 시간 분량의 다국어 오디오 데이터로 훈련되었습니다.
기존 STT 모델과 다른 점은 "범용성"입니다. 영어, 한국어, 일본어 등 99개 언어를 하나의 모델로 처리합니다. 언어별로 따로 모델을 설치할 필요가 없습니다. 잡음이 있는 환경에서도 비교적 잘 동작하고, 말하는 언어를 자동으로 감지할 수도 있습니다.
구조는 Transformer 기반 인코더-디코더입니다. 오디오를 30초 단위로 잘라서 멜 스펙트로그램(mel spectrogram)으로 변환하고, 이를 인코더에 넣어 특징을 추출한 뒤, 디코더가 텍스트를 생성합니다. 여기서 아키텍처 자체를 깊이 다루지는 않겠습니다. 중요한 것은 "잘 동작하는 오픈소스 STT 모델이 있다"는 사실입니다.
왜 whisper-cpp인가
OpenAI가 공개한 원본 Whisper는 Python + PyTorch로 되어 있습니다. 동작에 문제는 없지만, 몇 가지 불편한 점이 있습니다.
- Python 환경 설정이 필요합니다. 가상환경, PyTorch 버전, CUDA 의존성 등
- PyTorch 자체가 무겁습니다. 모델을 로드하는 데만 수 기가바이트의 메모리를 사용합니다
- CPU에서 실행하면 느립니다. GPU가 없으면 실시간 대비 5~10배 느린 속도
whisper.cpp는 Georgi Gerganov가 Whisper를 순수 C/C++로 재구현한 프로젝트입니다. 같은 사람이 LLM을 로컬에서 돌리는 llama.cpp도 만들었습니다. 두 프로젝트 모두 GGML이라는 경량 텐서 라이브러리 위에서 동작합니다.
whisper.cpp의 장점은 명확합니다.
- 의존성 최소: Python, PyTorch 불필요. 컴파일된 바이너리 하나면 됩니다. macOS에서는
brew install whisper-cpp로 설치 끝 - Apple Silicon 최적화: Metal API를 통해 GPU 가속을 지원합니다. M1/M2/M3 맥에서 Python 버전 대비 2~4배 빠른 속도
- 낮은 메모리 사용: GGML 포맷은 모델을 양자화(quantization)해서 메모리 사용을 줄입니다. large-v3 모델이 Python에서 ~6GB 쓰는 것에 비해, GGML 포맷은 ~1.5GB 수준
- CLI 친화적: 명령줄에서 바로 실행할 수 있어 스크립트나 파이프라인에 연결하기 좋습니다
모델 선택: large-v3-turbo
Whisper는 다양한 크기의 모델을 제공합니다.
| 모델 | 파라미터 | GGML 파일 크기 | 특징 |
|---|---|---|---|
| tiny | 39M | ~75MB | 가장 빠름, 정확도 낮음 |
| base | 74M | ~142MB | 간단한 용도 |
| small | 244M | ~466MB | 속도/정확도 균형 |
| medium | 769M | ~1.5GB | 대부분 충분 |
| large-v3 | 1.55B | ~3.1GB | 최고 정확도, 느림 |
| large-v3-turbo | 809M | ~1.6GB | large-v3에서 증류, 빠르면서 정확 |
large-v3-turbo를 선택한 이유는 가성비입니다.
이 모델은 large-v3를 "증류(distillation)"한 것입니다. 증류는 큰 모델(teacher)의 출력을 작은 모델(student)이 흉내 내도록 훈련하는 기법입니다. large-v3의 디코더가 32개 레이어인 것에 비해, turbo는 4개 레이어로 줄었습니다. 파라미터가 절반 가까이 줄면서 속도는 약 3배 빨라졌는데, 정확도 손실은 미미합니다.
구체적으로, M2 MacBook Air 기준 3분짜리 영상을 처리하는 데:
- large-v3: 약 40~50초
- large-v3-turbo: 약 15~20초
- medium: 약 10초 (하지만 비영어 구간 인식 정확도 차이)
영어에 한국어가 간간이 섞인 영상을 처리할 때, medium 이하에서는 한국어 구간을 영어로 잘못 인식하는 빈도가 눈에 띄게 늘었습니다. large-v3-turbo는 이런 다국어 전환을 비교적 잘 처리합니다.
실행 커맨드
whisper-cli \
-m ~/.local/share/whisper-cpp/ggml-large-v3-turbo.bin \
-l en \
--no-timestamps \
--vad \
--vad-model ~/.local/share/whisper-cpp/ggml-silero-vad.bin \
/tmp/audio.wav
각 옵션을 보면:
-m: 모델 파일 경로. GGML 포맷의 바이너리 파일-l en: 입력 언어를 영어로 지정. 자동 감지(-l auto)도 가능하지만, 주 언어를 알고 있으면 지정하는 게 정확도와 속도 면에서 유리합니다--no-timestamps: 타임스탬프 없이 순수 텍스트만 출력--vad/--vad-model: 다음 섹션에서 설명
Silero VAD — 말하는 구간만 골라내기
VAD란
VAD(Voice Activity Detection)는 오디오에서 사람이 실제로 말하고 있는 구간과 그렇지 않은 구간을 구분하는 기술입니다.
3분짜리 영상이라고 해서 3분 내내 말하고 있지는 않습니다. 말 사이의 침묵, 기침, 배경 소음, 키보드 소리 — 이런 비음성 구간이 전체의 30~50%를 차지하기도 합니다.
VAD 없이 STT를 돌리면
Whisper는 오디오를 30초 단위로 잘라서 처리합니다. VAD 없이 전체 오디오를 그대로 넣으면 두 가지 문제가 생깁니다.
첫째, 환각(hallucination)입니다. Whisper는 Transformer 기반 생성 모델입니다. 입력이 무음이어도 "뭔가를 생성하려는" 경향이 있습니다. 긴 침묵 구간에서 "Thank you for watching", "Please subscribe" 같은, 실제로 말하지 않은 텍스트를 만들어내는 경우가 있습니다. 훈련 데이터에 YouTube 영상이 많이 포함되어 있어서 이런 패턴이 학습된 것으로 보입니다.
둘째, 처리 시간 낭비입니다. 무음 구간도 30초 단위로 잘려서 모델에 들어갑니다. 전체 오디오의 절반이 침묵이라면, 처리 시간의 절반이 아무 의미 없는 구간에 소비됩니다.
Silero VAD의 동작
Silero VAD는 경량 신경망 기반 VAD 모델입니다. 모델 파일이 약 2MB로 매우 작고, CPU에서도 실시간의 수십 배 속도로 동작합니다.
동작 방식은 단순합니다. 오디오를 짧은 프레임(보통 30ms) 단위로 나누고, 각 프레임이 음성인지 아닌지를 0~1 사이의 확률로 판단합니다. 이 확률이 임계값(보통 0.5)을 넘는 구간만 "음성"으로 분류합니다.
전체 오디오: [──말──] [──침묵──] [──말──] [──침묵──] [──말──]
VAD 결과: [✓ 음성] [✗ 비음성] [✓ 음성] [✗ 비음성] [✓ 음성]
whisper 입력: [──말──] [──말──] [──말──]
whisper-cpp에서 --vad 플래그를 켜면, Silero VAD가 먼저 전체 오디오를 스캔해서 음성 구간만 추출하고, 그 구간만 Whisper 모델에 전달합니다.
결과적으로:
- 환각 감소: 무음 구간이 모델에 들어가지 않으므로 없는 말을 생성하는 문제가 줄어듭니다
- 속도 향상: 음성 구간만 처리하므로 전체 시간이 단축됩니다. 침묵이 많은 오디오일수록 효과가 큽니다
- 정확도 개선: 음성 시작/끝이 명확하게 잘려서 모델이 문맥을 더 잘 파악합니다
파이프라인 조합
세 도구를 연결하면 전체 흐름이 이렇게 됩니다.
# 1. 오디오 추출 (영상 → WAV)
ffmpeg -i ~/Downloads/video.MOV \
-ar 16000 -ac 1 -c:a pcm_s16le \
/tmp/stt-audio.wav -y
# 2. STT (WAV → 텍스트)
whisper-cli \
-m ~/.local/share/whisper-cpp/ggml-large-v3-turbo.bin \
-l en --no-timestamps \
--vad --vad-model ~/.local/share/whisper-cpp/ggml-silero-vad.bin \
/tmp/stt-audio.wav
# 3. 정리
rm /tmp/stt-audio.wav
커맨드 두 줄이 전부입니다. ffmpeg가 약 1초, whisper-cpp가 15~20초, 전체 30초 안에 3분짜리 영상의 텍스트가 나옵니다.
임시 파일을 /tmp에 두는 것은 의도적입니다. macOS에서 /tmp는 재부팅 시 자동으로 정리됩니다. 파이프라인이 중간에 실패해서 정리 단계가 실행되지 않더라도, 임시 파일이 영원히 남아있지 않습니다.
설치
macOS 기준으로 필요한 설치는 이것뿐입니다.
# 도구 설치
brew install ffmpeg whisper-cpp
# 모델 다운로드
mkdir -p ~/.local/share/whisper-cpp
# large-v3-turbo 모델 (~1.6GB)
curl -L -o ~/.local/share/whisper-cpp/ggml-large-v3-turbo.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin
# Silero VAD 모델 (~2MB)
curl -L -o ~/.local/share/whisper-cpp/ggml-silero-vad.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-silero-vad.bin
Python 환경 설정도, GPU 드라이버 설치도, Docker도 필요 없습니다. Homebrew로 두 패키지를 설치하고 모델 파일 두 개를 받으면 끝입니다.
로컬 STT vs 클라우드 API
마지막으로, 로컬 파이프라인과 클라우드 API의 트레이드오프를 정리하겠습니다.
| 로컬 (whisper-cpp) | 클라우드 API | |
|---|---|---|
| 비용 | 무료 (전기세 제외) | 분당 과금 (Whisper API: $0.006/분) |
| 프라이버시 | 파일이 로컬에 머무름 | 외부 서버로 전송 |
| 속도 | M2 Air 기준 실시간의 ~10배 | 네트워크 + 처리 시간 (보통 더 빠름) |
| 정확도 | 같은 Whisper 모델 기준 동일 | 동일 (또는 서버 GPU로 더 큰 모델 가능) |
| 의존성 | ffmpeg, whisper-cpp, 모델 파일 | API 키, 네트워크 |
| 오프라인 | 가능 | 불가능 |
| 배치 처리 | 파일 수 제한 없음 | API 할당량 제한 |
로컬이 무조건 낫다는 이야기가 아닙니다. 가끔 한두 번 쓸 거라면 API가 훨씬 간편합니다. 하지만 반복적으로 사용하고, 데이터를 외부에 보내고 싶지 않고, 오프라인에서도 작동해야 한다면 로컬 파이프라인이 합리적인 선택입니다.
마무리
ffmpeg, whisper-cpp, Silero VAD — 세 개의 도구를 연결해서 영상 파일의 음성을 텍스트로 변환하는 파이프라인을 만들었습니다.
각 단계를 정리하면:
- ffmpeg가 영상에서 오디오를 추출합니다. 16kHz 모노 PCM — Whisper가 기대하는 입력 포맷에 맞추기 위해서입니다.
- Silero VAD가 오디오에서 사람이 말하는 구간만 골라냅니다. 무음 구간에서의 환각을 줄이고 처리 속도를 높입니다.
- whisper-cpp가 음성을 텍스트로 변환합니다. C++ 구현 덕분에 Python 없이, Apple Silicon GPU 가속으로 빠르게 동작합니다.
도구를 설치하고 커맨드 두 줄을 실행하면 끝이라는 점에서, 로컬 STT의 진입 장벽은 예전보다 훨씬 낮아졌습니다. whisper.cpp와 같은 프로젝트들 덕분에, 예전이라면 클라우드 API에 의존해야 했던 작업을 맥북 한 대에서 오프라인으로 처리할 수 있게 되었습니다.
관련 글
Claude Code MCP 서버 정리 — 불필요한 도구 제거로 토큰 35% 절약하기
Claude Code의 MCP 서버 10개를 분석해 불필요한 4개를 제거했습니다. MCP 도구 정의가 매 메시지마다 시스템 프롬프트에 들어가 71K 토큰을 소비하고 있었다는 사실, 그리고 실제 정리 과정을 공유합니다.
Claude Code에서 블로그 글 작성하기: MCP를 직접 만들어 활용한 경험
Claude Code로 개발하면서 얻은 지식을 블로그에 정리하고 싶었습니다. 하지만 AI에게 블로그 관리자 권한을 모두 주기엔 불안했고, 필요한 API만 분기 처리하기엔 번거로웠습니다. MCP(Model Context Protocol)를 직접 만들어서 권한을 명확히 분리하고, Few-shot 예시와 SEO 가이드라인을 1회 호출로 제공하도록 개선한 경험을 공유합니다.
개인 블로그에 AI 검색 달기 (3) - 프론트엔드와 운영 최적화
React로 검색 모달 UI를 구현하고, Rate limiting으로 API 남용을 방지하는 과정. 디바운스, 키보드 네비게이션, 429 에러 핸들링까지 검색 UX 개선기.