1e2c004dd4
plan: ~/.claude/plans/luminous-sprouting-hamster.md §3
스키마:
- migrations/147_audio_segments_table.sql: audio_segments (STT 타임스탬프
세그먼트)
- migrations/148_audio_segments_idx.sql: (document_id, start_s) idx
- migrations/149_document_media_cols.sql: documents.thumbnail_path +
needs_conversion
- migrations/150_queue_stage_stt.sql: process_stage += 'stt'
- migrations/151_queue_stage_thumbnail.sql: process_stage += 'thumbnail'
- app/models/audio_segment.py, document.py (thumbnail_path/needs_conversion)
서비스:
- services/stt/{Dockerfile, requirements.txt, server.py} — faster-whisper
large-v3 GPU 컨테이너. /transcribe (filePath/langs/beamSize) +
/health + /ready (cuda device_count + model_loaded). NFC/NFD 경로
resolver (OCR 교훈).
- docker-compose.yml: stt-service 추가 (GPU 1 예약, :3300, NAS ro mount,
stt_models volume, start_period 300s), fastapi env 에 STT_ENDPOINT.
파이프라인 (의존 §1 category):
- app/workers/stt_worker.py 신규: stage='stt' pickup → STT_ENDPOINT 호출 →
extracted_text + audio_segments 저장. Timeout 30분.
- app/workers/thumbnail_worker.py 신규: ffmpeg 50% 지점 1장 →
PKM/Videos/.thumbs/{id}.jpg + thumbnail_path 세팅.
needs_conversion=true 는 skip.
- app/workers/file_watcher.py 확장: PKM/{Inbox, Recordings, Videos}
스캔. 확장자→category, audio→stage=stt, video .mp4/.webm→
stage=thumbnail, video .mov/.mkv/.avi→needs_conversion=true + stage
없음. settings.roon_library_path prefix skip.
- app/workers/queue_consumer.py 확장: stt + thumbnail workers 등록,
BATCH_SIZE(stt=1, thumbnail=3), next_stages 에 stt→[classify] 추가
(audio 는 extract 건너뜀).
- app/Dockerfile: ffmpeg 추가 (썸네일 subprocess 용).
API (의존 §1):
- /api/audio/{id}/segments — AudioSegment ORDER BY start_s
- /api/video/{id}/thumbnail — thumbnail_path FileResponse (쿼리 토큰)
- /api/documents/{id}/file: media_types 에 audio/video mime 포함 (§2
커밋에 이미 포함). Starlette FileResponse 가 Range 자동.
- upload_document: .mov/.mkv/.avi 웹 업로드 거부 (error_code
unsupported_codec). NAS 드롭은 file_watcher 가 quarantine 수용.
프론트:
- AudioPlayer.svelte: HTML5 audio + 전사 세그먼트 sticky 패널 + 줄
클릭 seek. activeIdx 하이라이트.
- VideoPlayer.svelte: HTML5 video direct play + needs_conversion 안내
카드. poster 는 thumbnail endpoint.
- /audio (목록 grid) + /audio/[id] (플레이어)
- /video (썸네일 grid + 변환 필요 배지) + /video/[id] (플레이어)
- Sidebar.svelte: Mic/Film 아이콘 + audio/video 네비 활성, count
배지 (§2 /stats/category-counts 재사용).
설정:
- app/core/config.py: stt_endpoint + roon_library_path.
DoD 배포 후 smoke: /ready cuda:true, 회의 mp3 transcribe, audio
extract 없이 classify 진행(queue 회귀), /audio 재생, .mp4 재생,
.mov 웹 400, .mov NAS quarantine, Sidebar 네비 + count.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
184 lines
4.7 KiB
YAML
184 lines
4.7 KiB
YAML
services:
|
|
postgres:
|
|
image: pgvector/pgvector:pg16
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
- ./migrations:/docker-entrypoint-initdb.d
|
|
environment:
|
|
POSTGRES_DB: pkm
|
|
POSTGRES_USER: pkm
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
ports:
|
|
- "15432:5432"
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U pkm"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 5
|
|
restart: unless-stopped
|
|
|
|
kordoc-service:
|
|
build: ./services/kordoc
|
|
ports:
|
|
- "3100:3100"
|
|
volumes:
|
|
- ${NAS_NFS_PATH:-/mnt/nas/Document_Server}:/documents:ro
|
|
mem_limit: 4g
|
|
memswap_limit: 4g
|
|
healthcheck:
|
|
test: ["CMD", "node", "-e", "fetch('http://localhost:3100/health').then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 3
|
|
restart: unless-stopped
|
|
|
|
ocr-service:
|
|
build: ./services/ocr
|
|
expose:
|
|
- "3200"
|
|
volumes:
|
|
- ${NAS_NFS_PATH:-/mnt/nas/Document_Server}:/documents:ro
|
|
- ocr_models:/root/.cache
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: 1
|
|
capabilities: [gpu]
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3200/health')"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 180s
|
|
restart: unless-stopped
|
|
|
|
stt-service:
|
|
build: ./services/stt
|
|
expose:
|
|
- "3300"
|
|
volumes:
|
|
- ${NAS_NFS_PATH:-/mnt/nas/Document_Server}:/documents:ro
|
|
- stt_models:/root/.cache
|
|
environment:
|
|
- WHISPER_MODEL=${WHISPER_MODEL:-large-v3}
|
|
- WHISPER_DEVICE=${WHISPER_DEVICE:-cuda}
|
|
- WHISPER_COMPUTE_TYPE=${WHISPER_COMPUTE_TYPE:-float16}
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: 1
|
|
capabilities: [gpu]
|
|
healthcheck:
|
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3300/health')"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 300s
|
|
restart: unless-stopped
|
|
|
|
ollama:
|
|
image: ollama/ollama
|
|
volumes:
|
|
- ollama_data:/root/.ollama
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: 1
|
|
capabilities: [gpu]
|
|
ports:
|
|
- "127.0.0.1:11434:11434"
|
|
restart: unless-stopped
|
|
|
|
# Phase 1.3: bge-reranker-v2-m3 (TEI) — internal only, fastapi에서 reranker:80으로 호출
|
|
# fastapi가 depends_on 안 함 → 단독 시작 가능, 없어도 fastapi 동작 (rerank=false fallback)
|
|
reranker:
|
|
image: ghcr.io/huggingface/text-embeddings-inference:1.7
|
|
container_name: hyungi_document_server-reranker-1
|
|
expose:
|
|
- "80"
|
|
environment:
|
|
- MODEL_ID=BAAI/bge-reranker-v2-m3
|
|
- MAX_BATCH_TOKENS=8192
|
|
- MAX_CONCURRENT_REQUESTS=4
|
|
volumes:
|
|
- reranker_cache:/data
|
|
deploy:
|
|
resources:
|
|
reservations:
|
|
devices:
|
|
- driver: nvidia
|
|
count: 1
|
|
capabilities: [gpu]
|
|
restart: unless-stopped
|
|
|
|
ai-gateway:
|
|
build: ./gpu-server/services/ai-gateway
|
|
ports:
|
|
- "127.0.0.1:8081:8080"
|
|
environment:
|
|
- PRIMARY_ENDPOINT=http://100.76.254.116:8801/v1/chat/completions
|
|
- FALLBACK_ENDPOINT=http://ollama:11434/v1/chat/completions
|
|
- CLAUDE_API_KEY=${CLAUDE_API_KEY:-}
|
|
- DAILY_BUDGET_USD=${DAILY_BUDGET_USD:-5.00}
|
|
depends_on:
|
|
- ollama
|
|
restart: unless-stopped
|
|
|
|
fastapi:
|
|
build: ./app
|
|
ports:
|
|
- "8000:8000"
|
|
volumes:
|
|
- ${NAS_NFS_PATH:-/mnt/nas/Document_Server}:/documents
|
|
- ./config.yaml:/app/config.yaml:ro
|
|
- ./scripts:/app/scripts:ro
|
|
- ./logs:/app/logs
|
|
- ./migrations:/app/migrations:ro
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
kordoc-service:
|
|
condition: service_healthy
|
|
env_file:
|
|
- credentials.env
|
|
environment:
|
|
- DATABASE_URL=postgresql+asyncpg://pkm:${POSTGRES_PASSWORD}@postgres:5432/pkm
|
|
- KORDOC_ENDPOINT=http://kordoc-service:3100
|
|
- OCR_ENDPOINT=http://ocr-service:3200
|
|
- STT_ENDPOINT=http://stt-service:3300
|
|
restart: unless-stopped
|
|
|
|
frontend:
|
|
build: ./frontend
|
|
ports:
|
|
- "3000:3000"
|
|
depends_on:
|
|
- fastapi
|
|
restart: unless-stopped
|
|
|
|
caddy:
|
|
image: caddy:2
|
|
ports:
|
|
- "8080:80"
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile
|
|
- caddy_data:/data
|
|
depends_on:
|
|
- fastapi
|
|
- frontend
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
pgdata:
|
|
caddy_data:
|
|
ollama_data:
|
|
reranker_cache:
|
|
ocr_models:
|
|
stt_models:
|