586 lines
16 KiB
Markdown
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 모델 서버 |
|