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