Files
whisper-stt/README.md

586 lines
16 KiB
Markdown

# VoiceScript — 음성 변환(STT) + 이미지 인식(OCR) 통합 툴
> **Debian OS + Docker Compose** 기반 자체 호스팅 서비스
> faster-whisper(STT) + PaddleOCR 3.x / Ollama Vision(OCR) 듀얼 백엔드
---
## 목차
1. [기능 개요](#기능-개요)
2. [프로젝트 구조](#프로젝트-구조)
3. [시스템 요구사항](#시스템-요구사항)
4. [설치 전 필수 확인사항 ⚠️](#설치-전-필수-확인사항)
5. [환경 변수 설정](#환경-변수-설정)
6. [빌드 및 실행](#빌드-및-실행)
7. [Nginx 연동 SSL](#nginx-연동-ssl)
8. [Ollama 모델 준비](#ollama-모델-준비)
9. [운영 관리](#운영-관리)
10. [트러블슈팅 알려진 이슈](#트러블슈팅-알려진-이슈)
11. [API 엔드포인트](#api-엔드포인트)
---
## 기능 개요
### 🎙 STT — 음성 텍스트 변환
- **엔진**: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) (OpenAI Whisper 최적화 포크)
- 지원 형식: `mp3` `wav` `m4a` `ogg` `flac` `aac` `mp4` `webm` `mkv`
- VAD(무음 구간 자동 제거) 적용
- 타임스탬프 세그먼트 분리 출력
- TXT 파일 다운로드
### 🔍 OCR — 이미지 텍스트 인식
- 지원 형식: `jpg` `png` `bmp` `tiff` `webp` `gif`
- **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를 확인하여 설정하세요.
```bash
ip addr show | grep "inet " | grep -v 127.0.0.1
```
`docker-compose.yml` 두 곳(app, worker) 모두 변경:
```yaml
- OLLAMA_URL=http://실제호스트IP:11434
```
### 2. 인증 정보 변경
```yaml
# app, worker 두 서비스 모두 동일하게 변경
- AUTH_USERNAME=원하는아이디
- AUTH_PASSWORD=강력한비밀번호
- JWT_SECRET=랜덤문자열 # openssl rand -hex 32
```
```bash
# JWT 시크릿 생성
openssl rand -hex 32
```
### 3. 포트 충돌 확인
```bash
ss -tlnp | grep 8800
```
충돌 시 `docker-compose.yml`에서 변경:
```yaml
ports:
- "원하는포트:8000"
```
### 4. 디스크 용량 확인
| 항목 | 크기 | 시점 |
|------|------|------|
| Whisper medium 모델 | ~1.5GB | 첫 STT 실행 시 자동 다운로드 |
| PaddleOCR korean 모델 | ~700MB | 첫 OCR 실행 시 자동 다운로드 |
| PaddlePaddle 3.0.0 | ~300MB | 빌드 시 |
| Docker 이미지 | ~3GB | 빌드 시 |
```bash
df -h /
# 여유 공간 10GB 이상 권장
```
### 5. Ollama 서버 실행 확인
```bash
curl http://localhost:11434/api/tags
# 응답 없으면 Ollama 미실행 상태
```
### 6. Docker Compose v2 확인
```bash
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`=무제한 |
---
## 빌드 및 실행
```bash
# 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
```
### 이후 코드 변경 시 재배포
```bash
# 코드만 변경된 경우 (재빌드 필요)
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 운용 중인 경우:
```nginx
# /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;
}
```
```bash
sudo certbot --nginx -d stt.yourdomain.com
sudo nginx -t && sudo systemctl reload nginx
```
---
## Ollama 모델 준비
호스트에서 미리 pull:
```bash
# 문서/표 특화 — 약 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` 이상으로 설정하세요.
---
## 운영 관리
```bash
# 상태 확인
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 이미지가 누적됩니다.
```bash
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` 추가
```yaml
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`으로 변경
```dockerfile
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()` 방식으로 변경
```python
# 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
```python
from ocr_tasks import ocr_task # noqa: F401
```
---
### ❌ `Unknown argument: use_gpu` / `Unknown argument: show_log`
**원인**: PaddleOCR 3.x에서 파라미터 제거됨
**해결**: `ocr_tasks.py`에서 해당 파라미터 삭제
```python
_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` 방식으로 파싱
```python
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에 옵션 추가
```yaml
command: redis-server --stop-writes-on-bgsave-error no
```
---
### ❌ Ollama 연결 타임아웃
**원인**: `host.docker.internal`이 Linux에서 불안정
**해결**: 실제 호스트 LAN IP로 변경
```yaml
- OLLAMA_URL=http://192.168.x.x:11434
```
---
### ❌ STT 진행률 5%/15%에서 멈춤
| 단계 | 원인 | 대기 시간 |
|------|------|---------|
| 5% `모델 준비 중` | Whisper 모델 첫 다운로드 (~1.5GB) | 5~20분 |
| 15% `오디오 분석 중` | 첫 변환 시 내부 초기화 | 1~3분 |
| `변환 중... Xs / Xs` | 정상 진행 | 파일 길이에 비례 |
```bash
# 진행 상황 실시간 확인
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 모델 서버 |