# 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 모델 서버 |