VoiceScript — 음성 변환(STT) + 이미지 인식(OCR) 통합 툴
Debian OS + Docker Compose 기반 자체 호스팅 서비스
faster-whisper(STT) + PaddleOCR 3.x / Ollama Vision(OCR) 듀얼 백엔드
목차
- 기능 개요
- 프로젝트 구조
- 시스템 요구사항
- 설치 전 필수 확인사항 ⚠️
- 환경 변수 설정
- 빌드 및 실행
- Nginx 연동 SSL
- Ollama 모델 준비
- 운영 관리
- 트러블슈팅 알려진 이슈
- API 엔드포인트
기능 개요
🎙 STT — 음성 텍스트 변환
- 엔진: faster-whisper (OpenAI Whisper 최적화 포크)
- 지원 형식:
mp3wavm4aoggflacaacmp4webmmkv등 - VAD(무음 구간 자동 제거) 적용
- 타임스탬프 세그먼트 분리 출력
- TXT 파일 다운로드
🔍 OCR — 이미지 텍스트 인식
- 지원 형식:
jpgpngbmptiffwebpgif - PaddleOCR 모드: 로컬 실행, 표 구조 분석(PP-Structure), Excel 다운로드
- Ollama Vision 모드: 기존 Ollama 서버 활용, 자연어 지시, 커스텀 프롬프트
🔐 인증
- JWT 기반 로그인 (만료 시간 설정 가능)
- 모든 API 토큰 인증 필수
프로젝트 구조
whisper-stt/
│
├── docker-compose.yml # 전체 서비스 정의
│
├── app/
│ ├── Dockerfile # Python 3.11-slim + ffmpeg + PaddlePaddle 3.0.0
│ ├── requirements.txt # Python 패키지 목록
│ │
│ ├── main.py # FastAPI 앱 (인증 + STT + OCR 엔드포인트)
│ ├── auth.py # JWT 인증 모듈
│ ├── tasks.py # Celery STT 태스크 (faster-whisper)
│ ├── ocr_tasks.py # Celery OCR 태스크 (PaddleOCR / Ollama)
│ │
│ └── static/
│ └── index.html # 웹 프론트엔드 (로그인 + STT + OCR 탭)
│
└── nginx/ # 참고용 (호스트 Nginx 사용 시 불필요)
├── Dockerfile
└── nginx.conf
컨테이너 구성
┌─────────────────────────────────────────┐
│ 호스트 Nginx (SSL/certbot) │
│ → 리버스 프록시 → 127.0.0.1:8800 │
└─────────────────────────────────────────┘
│
▼
┌──────────────────┐ ┌──────────────────────┐
│ whisper_app │ │ whisper_worker │
│ FastAPI:8000 │ │ Celery (solo pool) │
│ (포트 8800) │ │ STT + OCR 처리 │
└────────┬─────────┘ └──────────┬───────────┘
│ │
└────────────┬────────────┘
▼
┌──────────────────┐
│ whisper_redis │
│ Redis:6379 │
│ (작업 큐/결과) │
└──────────────────┘
시스템 요구사항
| 항목 | 최소 | 권장 |
|---|---|---|
| CPU | 4코어 | AMD 5825u 이상 |
| RAM | 8GB | 16GB (medium 모델 기준) |
| 디스크 | 20GB | 50GB 이상 |
| OS | Debian 11+ | Debian 12 (Bookworm) |
| Docker | 24.0+ | 최신 |
| Docker Compose | v2.0+ | 최신 (version: 필드 불필요) |
의존 서비스
- Ollama: 호스트에서
11434포트로 실행 중이어야 함 (OCR Vision 모드 사용 시)
설치 전 필수 확인사항
⚠️ 이 섹션을 건너뛰면 빌드 후 오류가 발생합니다.
1. 호스트 IP 확인 — OLLAMA_URL 설정
host.docker.internal은 Linux에서 동작하지 않습니다.
반드시 실제 LAN IP를 확인하여 설정하세요.
ip addr show | grep "inet " | grep -v 127.0.0.1
docker-compose.yml 두 곳(app, worker) 모두 변경:
- OLLAMA_URL=http://실제호스트IP:11434
2. 인증 정보 변경
# app, worker 두 서비스 모두 동일하게 변경
- AUTH_USERNAME=원하는아이디
- AUTH_PASSWORD=강력한비밀번호
- JWT_SECRET=랜덤문자열 # openssl rand -hex 32
# JWT 시크릿 생성
openssl rand -hex 32
3. 포트 충돌 확인
ss -tlnp | grep 8800
충돌 시 docker-compose.yml에서 변경:
ports:
- "원하는포트:8000"
4. 디스크 용량 확인
| 항목 | 크기 | 시점 |
|---|---|---|
| Whisper medium 모델 | ~1.5GB | 첫 STT 실행 시 자동 다운로드 |
| PaddleOCR korean 모델 | ~700MB | 첫 OCR 실행 시 자동 다운로드 |
| PaddlePaddle 3.0.0 | ~300MB | 빌드 시 |
| Docker 이미지 | ~3GB | 빌드 시 |
df -h /
# 여유 공간 10GB 이상 권장
5. Ollama 서버 실행 확인
curl http://localhost:11434/api/tags
# 응답 없으면 Ollama 미실행 상태
6. Docker Compose v2 확인
docker compose version
# v2.x 이상이어야 함 (docker-compose가 아닌 docker compose)
환경 변수 설정
docker-compose.yml의 app과 worker 두 서비스에 동일하게 설정.
인증
| 변수 | 기본값 | 설명 |
|---|---|---|
AUTH_USERNAME |
admin |
로그인 아이디 |
AUTH_PASSWORD |
changeme1234 |
로그인 비밀번호 변경 필수 |
JWT_SECRET |
(변경 필수) | JWT 서명 키 |
JWT_EXPIRE_HOURS |
12 |
토큰 유효 시간 (시간 단위) |
Whisper STT
| 변수 | 기본값 | 설명 |
|---|---|---|
WHISPER_MODEL |
medium |
tiny base small medium large-v3 |
WHISPER_DEVICE |
cpu |
GPU 없는 경우 cpu |
WHISPER_COMPUTE_TYPE |
int8 |
CPU 최적화: int8 권장 |
WHISPER_LANGUAGE |
ko |
언어 고정. 비우면 자동 감지 |
WHISPER_BEAM_SIZE |
5 |
정확도↑ vs 속도↓ |
WHISPER_INITIAL_PROMPT |
비어있음 | 도메인 힌트 예: "고객 상담 녹취록입니다." |
모델별 성능 (5825u CPU 기준)
| 모델 | 크기 | 1분 변환 시간 | 한국어 정확도 |
|---|---|---|---|
| tiny | 75MB | ~5초 | 보통 |
| base | 145MB | ~10초 | 보통 |
| small | 484MB | ~30초 | 양호 |
| medium | 1.5GB | ~90초 | 우수 ← 권장 |
| large-v3 | 3GB | ~5분+ | 최고 |
PaddleOCR
| 변수 | 기본값 | 설명 |
|---|---|---|
OCR_LANG |
korean |
korean en japan chinese_cht ch |
Ollama OCR
| 변수 | 기본값 | 설명 |
|---|---|---|
OLLAMA_URL |
http://192.168.0.126:11434 |
실제 호스트 IP로 변경 필수 |
OLLAMA_TIMEOUT |
180 |
초 단위. 11b 이상 모델은 300 이상 권장 |
파일 관리
| 변수 | 기본값 | 설명 |
|---|---|---|
MAX_UPLOAD_MB |
500 |
업로드 최대 파일 크기 (MB) |
OUTPUT_KEEP_HOURS |
48 |
결과 파일 보관 시간. 0=무제한 |
빌드 및 실행
# 1. 저장소 클론
git clone http://gitea.byunc.com/byun/whisper-stt.git
cd whisper-stt
# 2. 필수 설정 변경 (docker-compose.yml)
# - AUTH_USERNAME, AUTH_PASSWORD, JWT_SECRET
# - OLLAMA_URL (호스트 실제 IP)
# 3. 빌드 및 시작
docker compose up -d --build
# 4. 빌드 후 모델 다운로드 완료까지 대기
docker compose logs -f worker
# "[Whisper] 로드 완료" + "celery@... ready." 확인
접속:
http://서버IP:8800
이후 코드 변경 시 재배포
# 코드만 변경된 경우 (재빌드 필요)
docker compose build --no-cache app worker
docker compose up -d
# 환경변수만 변경된 경우 (재빌드 불필요)
docker compose up -d --force-recreate app worker
# Docker 이미지 정리 (빌드 반복 후 용량 정리)
docker system prune -f
Nginx 연동 SSL
호스트 Nginx + certbot SSL 운용 중인 경우:
# /etc/nginx/sites-available/voicescript.conf
server {
listen 443 ssl;
server_name stt.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/stt.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/stt.yourdomain.com/privkey.pem;
# ⚠️ 음성 파일 업로드를 위해 반드시 설정 (기본 1MB → 초과 시 413 에러)
client_max_body_size 500M;
client_body_timeout 300s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
location / {
proxy_pass http://127.0.0.1:8800;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name stt.yourdomain.com;
return 301 https://$host$request_uri;
}
sudo certbot --nginx -d stt.yourdomain.com
sudo nginx -t && sudo systemctl reload nginx
Ollama 모델 준비
호스트에서 미리 pull:
# 문서/표 특화 — 약 2GB ← 기본값, 권장
ollama pull granite3.2-vision
# OCR 전용 경량 — 약 2GB
ollama pull deepseek-ocr:3b
# 범용 고정확도 — 약 8GB (RAM 16GB+ 필요)
ollama pull llama3.2-vision:11b
# 최고 정확도 — 약 9GB (RAM 16GB+ 필요)
ollama pull richardyoung/olmocr2:7b-q8
참고:
granite3.2-vision만 설치되어 있어도 즉시 사용 가능합니다.
큰 모델 사용 시OLLAMA_TIMEOUT=300이상으로 설정하세요.
운영 관리
# 상태 확인
docker compose ps
# 로그 확인
docker compose logs app --tail=30
docker compose logs worker --tail=30
docker compose logs -f # 전체 실시간
# 재시작
docker compose restart
# 중지
docker compose down
# 설정 변경 후 재시작 (재빌드 없이)
docker compose up -d --force-recreate app worker
Docker 이미지 정리
빌드를 반복하면 dangling 이미지가 누적됩니다.
docker system df # 사용량 확인
docker system prune -f # 불필요한 이미지/컨테이너 정리
docker compose down -v # 볼륨 포함 완전 초기화 (모델 재다운로드 필요)
볼륨 정보
| 볼륨 | 내용 | 삭제 시 영향 |
|---|---|---|
whisper_models |
Whisper 모델 (~1.5GB) | 재다운로드 필요 |
paddle_models |
PaddleOCR 모델 (~700MB) | 재다운로드 필요 |
stt_data |
업로드/결과 파일 | 데이터 손실 |
redis_data |
작업 큐 상태 | 진행 중 작업 손실 |
트러블슈팅 알려진 이슈
실제 배포 과정에서 겪은 오류와 해결 방법입니다.
❌ signal 11 (SIGSEGV) — Worker 크래시
원인: faster-whisper 내부 CTranslate2 라이브러리가 Celery prefork 방식과 충돌
해결: docker-compose.yml worker command에 --pool=solo 추가
command: >
celery -A tasks worker
--loglevel=info
--pool=solo # ← 이 옵션이 핵심
--max-tasks-per-child=50
-Q stt,ocr
--pool=solo는 포크 없이 메인 프로세스에서 직접 실행합니다.
--concurrency=1이었으므로 성능 차이는 없습니다.
❌ No matching distribution found for paddlepaddle==2.6.1
원인: 미러에서 해당 버전 제거됨
해결: Dockerfile에서 3.0.0으로 변경
RUN pip install --no-cache-dir paddlepaddle==3.0.0 \
-i https://pypi.tuna.tsinghua.edu.cn/simple
❌ ValueError: password cannot be longer than 72 bytes
원인: passlib[bcrypt] 초기화 버그
해결: auth.py에서 bcrypt 완전 제거, 직접 문자열 비교 방식 사용
requirements.txt에서 passlib 줄 삭제
❌ AttributeError: 'DisabledBackend'
원인: from celery.result import AsyncResult 사용 시 백엔드 설정 누락
해결: celery_app.AsyncResult() 방식으로 변경
# main.py
from tasks import celery_app
r = celery_app.AsyncResult(task_id) # ✅
❌ ModuleNotFoundError: No module named 'ocr_tasks'
원인: celery_app.autodiscover_tasks(["ocr_tasks"]) 동작 안 함
해결: tasks.py에서 직접 import
from ocr_tasks import ocr_task # noqa: F401
❌ Unknown argument: use_gpu / Unknown argument: show_log
원인: PaddleOCR 3.x에서 파라미터 제거됨
해결: ocr_tasks.py에서 해당 파라미터 삭제
_ocr_engine = PaddleOCR(use_angle_cls=True, lang=OCR_LANG) # ✅
❌ PaddleOCR.predict() got an unexpected keyword argument 'cls'
원인: PaddleOCR 3.x API 변경
해결: ocr(img, cls=True) → ocr(img)
❌ 'AnalysisConfig' object has no attribute 'set_optimization_level'
원인: PaddleOCR 3.x와 paddlepaddle 2.x 버전 불일치
해결: paddlepaddle 3.0.0으로 업그레이드
❌ too many values to unpack (expected 2)
원인: PaddleOCR 3.x 결과 구조 변경
해결: rec_texts / rec_scores 방식으로 파싱
r = result[0]
texts = r.get("rec_texts", [])
scores = r.get("rec_scores", [])
❌ MISCONF Redis is configured to save RDB snapshots
원인: 디스크 부족으로 Redis RDB 저장 실패 → 쓰기 차단
해결: docker-compose.yml Redis command에 옵션 추가
command: redis-server --stop-writes-on-bgsave-error no
❌ Ollama 연결 타임아웃
원인: host.docker.internal이 Linux에서 불안정
해결: 실제 호스트 LAN IP로 변경
- OLLAMA_URL=http://192.168.x.x:11434
❌ STT 진행률 5%/15%에서 멈춤
| 단계 | 원인 | 대기 시간 |
|---|---|---|
5% 모델 준비 중 |
Whisper 모델 첫 다운로드 (~1.5GB) | 5~20분 |
15% 오디오 분석 중 |
첫 변환 시 내부 초기화 | 1~3분 |
변환 중... Xs / Xs |
정상 진행 | 파일 길이에 비례 |
# 진행 상황 실시간 확인
docker compose logs worker -f
API 엔드포인트
인증
| 메서드 | 경로 | 설명 |
|---|---|---|
POST |
/api/login |
로그인 (username, password form) |
GET |
/api/me |
현재 사용자 확인 |
STT
| 메서드 | 경로 | 설명 |
|---|---|---|
POST |
/api/transcribe |
음성 파일 업로드 및 변환 시작 |
GET |
/api/status/{task_id} |
작업 진행 상태 조회 |
GET |
/api/download/{filename} |
결과 파일 다운로드 |
OCR
| 메서드 | 경로 | 설명 |
|---|---|---|
POST |
/api/ocr |
이미지 업로드 및 인식 시작 |
OCR 파라미터
| 파라미터 | 기본값 | 설명 |
|---|---|---|
file |
— | 이미지 파일 |
mode |
text |
text | structure |
backend |
paddle |
paddle | ollama |
ollama_model |
granite3.2-vision |
Ollama 모델명 |
custom_prompt |
비어있음 | Ollama 커스텀 프롬프트 |
관리
| 메서드 | 경로 | 설명 |
|---|---|---|
POST |
/api/cleanup |
오래된 결과 파일 정리 |
기술 스택
| 구성요소 | 버전 | 역할 |
|---|---|---|
| Python | 3.11 | 런타임 |
| FastAPI | 0.115 | API 서버 |
| Celery | 5.4 (--pool=solo) |
비동기 태스크 큐 |
| Redis | 7 alpine | 메시지 브로커 |
| faster-whisper | 1.0.3 | STT 엔진 |
| PaddlePaddle | 3.0.0 | OCR 딥러닝 프레임워크 |
| PaddleOCR | 3.x | OCR 엔진 |
| httpx | 0.27+ | Ollama API 호출 |
| Ollama | 호스트 운용 | Vision 모델 서버 |