feat: VoiceScript STT+OCR 초기 버전

This commit is contained in:
root
2026-04-20 06:15:35 +09:00
commit ddd51da26e
11 changed files with 2163 additions and 0 deletions

601
README.md Normal file
View File

@@ -0,0 +1,601 @@
# 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
│ ├── 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 Worker │
│ (포트 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+ | 최신 |
### 의존 서비스
- **Ollama** (OCR Vision 모드 사용 시): 호스트에서 `11434` 포트로 실행 중이어야 함
---
## 설치 전 필수 확인사항
> ⚠️ 이 섹션을 건너뛰면 빌드 후 오류가 발생할 수 있습니다.
### 1. 호스트 IP 확인 — OLLAMA_URL 설정
Docker 컨테이너에서 호스트의 Ollama에 접근할 때 `host.docker.internal`
**Linux에서 동작하지 않을 수 있습니다.** 반드시 실제 LAN IP를 사용하세요.
```bash
# 호스트 IP 확인
ip addr show | grep "inet " | grep -v 127.0.0.1
```
`docker-compose.yml`에서 아래 두 곳을 실제 IP로 변경:
```yaml
- OLLAMA_URL=http://실제호스트IP:11434
```
### 2. 인증 정보 변경 — 기본값 절대 사용 금지
```yaml
# docker-compose.yml — app, worker 두 곳 모두 변경
- AUTH_USERNAME=원하는아이디
- AUTH_PASSWORD=강력한비밀번호
- JWT_SECRET=랜덤한긴문자열 # openssl rand -hex 32 로 생성 권장
```
JWT 시크릿 생성:
```bash
openssl rand -hex 32
```
### 3. 포트 충돌 확인
기본 포트 `8800`이 사용 중인지 확인:
```bash
ss -tlnp | grep 8800
```
충돌 시 `docker-compose.yml`에서 변경:
```yaml
ports:
- "원하는포트:8000"
```
### 4. 디스크 용량 확인
첫 빌드 시 자동 다운로드 크기:
| 모델 | 크기 | 비고 |
|------|------|------|
| Whisper medium | ~1.5GB | HuggingFace 자동 다운로드 |
| PaddleOCR korean | ~200MB | 첫 실행 시 자동 다운로드 |
| PaddlePaddle 3.0.0 | ~300MB | 빌드 시 pip install |
```bash
df -h /
```
여유 공간 **5GB 이상** 권장.
### 5. Ollama 서버 실행 확인
```bash
curl http://localhost:11434/api/tags
```
응답이 없으면 Ollama가 실행 중이지 않은 것입니다.
### 6. Redis 포트 충돌 확인
이 프로젝트의 Redis(`whisper_redis`)는 **내부 네트워크 전용**이라 호스트에 포트를 노출하지 않습니다. 별도 포트 충돌 없음.
---
## 환경 변수 설정
`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/[사용자명]/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. 진행 상황 확인 (Whisper 모델 다운로드 완료까지 대기)
docker compose logs -f worker
```
`[Whisper] 로드 완료``celery@... ready.` 메시지 확인 후 접속:
```
http://서버IP:8800
```
---
## Nginx 연동 (SSL)
호스트 Nginx + certbot으로 SSL을 운용 중인 경우:
### Nginx 설정 (`/etc/nginx/sites-available/voicescript.conf`)
```nginx
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;
# 대용량 음성 파일 업로드 허용
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
# SSL 인증서 발급
sudo certbot --nginx -d stt.yourdomain.com
# 설정 적용
sudo nginx -t && sudo systemctl reload nginx
```
> **주의**: Nginx에서 `client_max_body_size 500M`을 반드시 설정하세요.
> 기본값(1MB)이면 음성 파일 업로드가 413 에러로 실패합니다.
---
## Ollama 모델 준비
### 권장 OCR 모델 (호스트에서 미리 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`이 설치되어 있으면 즉시 사용 가능합니다.
---
## 운영 관리
### 기본 명령어
```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 image ls | grep whisper
docker image rm [IMAGE_ID]
```
### 볼륨 관리
| 볼륨 | 내용 | 삭제 시 영향 |
|------|------|-------------|
| `whisper_models` | Whisper 모델 (~1.5GB) | 재다운로드 필요 |
| `paddle_models` | PaddleOCR 모델 (~700MB) | 재다운로드 필요 |
| `stt_data` | 업로드/결과 파일 | 데이터 손실 |
| `redis_data` | 작업 큐 상태 | 진행 중 작업 손실 |
```bash
# 모델 캐시 포함 완전 초기화 (주의!)
docker compose down -v
```
### 결과 파일 수동 정리
```bash
# API로 정리 (OUTPUT_KEEP_HOURS 기준)
curl -X POST http://localhost:8800/api/cleanup \
-H "Authorization: Bearer [토큰]"
```
---
## 트러블슈팅 (알려진 이슈)
실제 배포 과정에서 겪은 오류들과 해결 방법입니다.
---
### ❌ `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` (bcrypt)
**원인**: `passlib[bcrypt]` 초기화 시 버그
**해결**: `auth.py`에서 bcrypt 완전 제거, 직접 문자열 비교로 대체
`requirements.txt`에서 `passlib` 줄 삭제
---
### ❌ `AttributeError: 'DisabledBackend' object has no attribute '_get_task_meta_for'`
**원인**: `main.py`에서 `from celery.result import AsyncResult` 사용 시 백엔드 설정이 누락됨
**해결**: `celery_app.AsyncResult(task_id)` 방식으로 변경
```python
# main.py
from tasks import celery_app
r = celery_app.AsyncResult(task_id) # ✅
# AsyncResult(task_id) # ❌
```
---
### ❌ `ModuleNotFoundError: No module named 'ocr_tasks'`
**원인**: `tasks.py``celery_app.autodiscover_tasks(["ocr_tasks"])` 동작 안 함
**해결**: `autodiscover_tasks` 제거, 직접 import로 변경
```python
# tasks.py 상단에 추가
from ocr_tasks import ocr_task # noqa: F401
```
---
### ❌ `KeyError: 'tasks.ocr_task'`
**원인**: worker에 ocr_task가 등록되지 않음 (위와 동일 원인)
**해결**: 위와 동일 (`from ocr_tasks import ocr_task`)
---
### ❌ `Unknown argument: use_gpu` / `Unknown argument: show_log`
**원인**: PaddleOCR 3.x에서 파라미터 제거됨
**해결**: `ocr_tasks.py`에서 해당 파라미터 삭제
```python
# ✅ PaddleOCR 3.x
_ocr_engine = PaddleOCR(use_angle_cls=True, lang=OCR_LANG)
# ❌ 구버전 방식
_ocr_engine = PaddleOCR(use_angle_cls=True, lang=OCR_LANG, use_gpu=False, show_log=False)
```
---
### ❌ `PaddleOCR.predict() got an unexpected keyword argument 'cls'`
**원인**: PaddleOCR 3.x에서 API 변경
**해결**: `ocr(img, cls=True)``ocr(img)`
```python
result = get_ocr().ocr(img) # ✅ PaddleOCR 3.x
```
---
### ❌ `'paddle.base.libpaddle.AnalysisConfig' object has no attribute 'set_optimization_level'`
**원인**: PaddleOCR 3.x와 paddlepaddle 2.x 버전 불일치
**해결**: `Dockerfile`에서 paddlepaddle `3.0.0`으로 업그레이드
---
### ❌ `too many values to unpack (expected 2)`
**원인**: PaddleOCR 3.x 결과 구조 변경 (`[bbox, (text, conf)]` → dict 형태)
**해결**: `ocr_tasks.py`의 결과 파싱 로직 변경
```python
# ✅ PaddleOCR 3.x
r = result[0]
texts = r.get("rec_texts", [])
scores = r.get("rec_scores", [])
# ❌ 구버전
for bbox, (text, conf) in result[0]:
...
```
---
### ❌ `MISCONF Redis is configured to save RDB snapshots...`
**원인**: 디스크 용량 부족 또는 권한 문제로 Redis RDB 저장 실패 → 쓰기 차단
**해결**: `docker-compose.yml` Redis에 옵션 추가 (Celery 브로커 용도는 영속성 불필요)
```yaml
redis:
command: redis-server --stop-writes-on-bgsave-error no
```
---
### ❌ Ollama 연결 타임아웃 (`ConnectError`)
**원인**: `host.docker.internal`이 Linux에서 동작하지 않음
**해결**: `OLLAMA_URL`을 호스트의 실제 LAN IP로 변경
```yaml
- OLLAMA_URL=http://192.168.x.x:11434 # ✅ 실제 IP
# - OLLAMA_URL=http://host.docker.internal:11434 # ❌ Linux에서 불안정
```
---
### ❌ STT 5%에서 멈춤 (진행 없음)
**원인**: Whisper 모델 첫 다운로드 중 (HuggingFace에서 ~1.5GB)
**해결**: 기다리면 됩니다. worker 로그로 진행 확인:
```bash
docker compose logs -f worker
```
`[Whisper] 로드 완료` 메시지 나올 때까지 대기 (네트워크 속도에 따라 5~20분).
---
## API 엔드포인트
### 인증
| 메서드 | 경로 | 설명 |
|--------|------|------|
| `POST` | `/api/login` | 로그인 (form: `username`, `password`) |
| `GET` | `/api/me` | 현재 사용자 확인 |
### STT
| 메서드 | 경로 | 설명 |
|--------|------|------|
| `POST` | `/api/transcribe` | 음성 파일 업로드 및 변환 시작 |
| `GET` | `/api/status/{task_id}` | 작업 진행 상태 조회 |
| `GET` | `/api/download/{filename}` | 결과 파일 다운로드 |
### OCR
| 메서드 | 경로 | 설명 |
|--------|------|------|
| `POST` | `/api/ocr` | 이미지 업로드 및 인식 시작 |
**OCR POST 파라미터**
| 파라미터 | 기본값 | 설명 |
|---------|--------|------|
| `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 | 비동기 태스크 큐 |
| 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 모델 서버 |