아키텍처
핵심 컨테이너 3개에 mDNS 서비스를 더한 구성으로, 별도의 메시지 브로커나 클라우드 서비스가 필요 없습니다. 업로드하거나 NAS 폴더에서 가져온 사진은 메타데이터, 썸네일, 시맨틱 벡터, 얼굴, 문자 순으로 처리되어 자연어·인물·장소로 검색할 수 있는 보관함이 됩니다.
Go 백엔드, AI 서비스, PostgreSQL, 그리고 LAN 도메인을 알리는 mDNS
metadata, thumbnail, clip, face, ocr
Chinese-CLIP 이미지·텍스트 벡터(모델 등급에 따라 512·768·1024차원), ArcFace 얼굴 특징은 512차원
pgvector 코사인 거리 인덱스. 검색과 얼굴 비교를 데이터베이스 안에서 수행
시스템 구조
브라우저는 Go 백엔드에만 연결합니다. 백엔드가 업로드, 작업 스케줄링, 썸네일, 권한 관리를 맡고, 미리 보기 이미지를 AI 서비스에 넘겨 추론한 뒤 결과를 모두 PostgreSQL에 기록합니다. AI 서비스는 상태가 없어 데이터베이스에 접근하지도 파일을 저장하지도 않으므로, 더 강력한 기기에 따로 배포할 수 있습니다.
웹 앱은 빌드되어 프로그램에 내장되므로 배포에 Node가 필요 없습니다. 시작할 때 데이터베이스 마이그레이션이 자동으로 실행되며, 업그레이드는 이미지만 바꾸면 됩니다.
내장 AI 서비스는 Docker 내부 네트워크에서 CPU로 추론합니다. 웹에서 원격 주소를 입력하면 집에 있는 그래픽 카드 PC나 Mac으로 전환할 수 있습니다.
사진 정보, 인물, 설정, 작업 큐, 벡터를 모두 PostgreSQL에 저장하므로 업무 데이터와 작업 상태는 함께 백업할 수 있습니다. 사진 파일은 따로 백업해야 합니다.
사진 처리 파이프라인
사진을 가져오면 먼저 메타데이터를 추출하고 썸네일을 만든 뒤, 시맨틱 벡터 계산과 얼굴·문자 인식을 병렬로 진행합니다. 각 단계는 따로 큐에 들어가므로 재시작하거나 AI 서비스가 끊겨도 작업이 사라지지 않고, 복구되면 이어서 처리합니다.
ingest- 웹 업로드는 tus 사용, 끊겨도 이어 올리기
- /photos 폴더를 15분마다 스캔
- SHA-1 체크섬을 계산해 assets에 기록
metadata- ExifTool로 크기와 촬영 시각 읽기
- GPS, 카메라, 렌즈, 조리개와 셔터
- 시간대가 없으면 설정한 시간대로 계산
thumbnail- libvips로 썸네일 생성
- 이어서 1440px 미리 보기 생성
- 동영상은 FFmpeg로 프레임 추출·변환
clip- Chinese-CLIP으로 1024차원 벡터 계산
- 자연어 검색, 자동 분류
- 미적 점수, 비슷한 사진
face- SCRFD로 얼굴 검출, 특징점 5개로 정렬
- ArcFace로 512차원 특징 계산
- 인물로 점진적 분류
ocr- PP-OCR로 사진 속 글자 인식
- 글자로 사진 검색
- 신분증, 문서, 스크린샷 판별
백그라운드 작업 스케줄링
별도의 메시지 브로커는 없습니다. 작업 큐는 PostgreSQL의 jobs 테이블이며, 사진 데이터와 같은 트랜잭션으로 기록되어 사진이 저장되면 작업도 반드시 큐에 들어갑니다. 워커는 SKIP LOCKED로 작업을 가져가고, 임대 기간으로 멈춤을 막으며, 실패하면 제곱 백오프로 재시도합니다.
-- 작업 하나 가져오기 (server/internal/jobs/worker.go)
UPDATE jobs SET status = 'running', attempts = attempts + 1,
locked_by = $1, locked_until = now() + $2::interval
WHERE id = (
SELECT id FROM jobs
WHERE status = 'pending' AND run_after <= now() AND type = ANY($3)
ORDER BY priority DESC, run_after
FOR UPDATE SKIP LOCKED -- 여러 워커가 동시에 가져가도 같은 작업을 잡지 않음
LIMIT 1
)
RETURNING id, type, asset_id, payload, attempts, max_attempts;- 같은 사진의 같은 종류 작업이 이미 대기 중이거나 실행 중이면 다시 큐에 넣지 않습니다.
- 큐가 비어 있으면 1초마다 확인하고, priority가 높은 순, 예정 시각이 이른 순으로 가져갑니다.
- 각 작업의 임대 기간(시간 제한 겸용)은 10분입니다. 프로세스가 재시작되면 자기 몫의 끝나지 않은 작업을 큐로 되돌립니다.
- 누군가 사진을 업로드하는 동안에는 AI 작업 가져가기를 멈추고 대역폭과 CPU를 업로드에 먼저 내줍니다.
- AI 서비스에 연결할 수 없으면 AI 워커 전체가 멈춰 기다립니다. 작업을 하나씩 꺼내 실패시키며 재시도 횟수를 헛되이 쓰지 않습니다.
- 「자동」 등급은 먼저 AI 서버의 하드웨어를 확인해 알맞은 모델을 정한 뒤 작업을 가져갑니다. 라즈베리 파이 같은 기기에서 사용 가능한 메모리를 넘는 모델을 불러오지 않도록 돕습니다.
AI 스케줄링
AI 서비스는 미리 보기 이미지를 받아 얼굴, 벡터, 문자를 돌려주는 일만 합니다. 서비스 주소, 모델, 동시 처리 수는 백엔드가 정하고, AI 서비스는 하드웨어에 따라 CUDA, CoreML / MPS, OpenVINO, CPU 중에서 고릅니다.
「자동」을 선택하면 AI 서버의 하드웨어에 따라 추천합니다. 쓸 수 있는 그래픽 카드가 있으면 고정밀, CPU만 있으면 메모리 6 GB 미만은 경량, 6 GB 이상은 균형.
| 등급 | 알맞은 하드웨어 | 얼굴 | 시맨틱 검색 |
|---|---|---|---|
| quality · 고정밀 | Apple Silicon Mac, NVIDIA 그래픽 카드 | SCRFD-10G + ArcFace R100 (antelopev2) | Chinese-CLIP ViT-H/14 · 1024차원 |
| balanced · 균형 | Intel i5급 NAS | SCRFD-10G + ArcFace R100 (antelopev2) | Chinese-CLIP ViT-L/14 · 768차원 |
| lite · 경량 | N100, 라즈베리 파이 5 | SCRFD + ArcFace R50 (buffalo_l) | Chinese-CLIP ViT-B/16 · 512차원 |
POST /predict (multipart: image + tasks)
Authorization: Bearer <토큰>
tasks = {
"face": {"model": "antelopev2", "min_score": 0.5},
"clip": {"model": "chinese-clip-vit-h-14"},
"ocr": {}
}
→ {
"face": [{"box": [x, y, w, h], "score": 0.93,
"embedding": [숫자 512개], "yaw": 0.12, "blur": 340}],
"clip": {"embedding": [숫자 1024개]},
"ocr": {"text": "...", "lines": [...]},
"model_ids": {"face": "antelopev2", "clip": "chinese-clip-vit-h-14"}
}핵심 알고리즘
검색, 인물, 연사, AI 추천 뒤에서 쓰이는 모델, 알고리즘, 임곗값입니다. AI 모델에 기대는 것도 있고, 벡터 계산과 규칙으로 해결하는 것도 있습니다.
Chinese-CLIP은 사진과 글을 같은 벡터 공간에 놓습니다. 사진을 가져올 때 이미지 벡터를 미리 계산해 두고, 검색할 때는 「아기 세 돌 생일」 같은 문장을 텍스트 벡터로 바꿔 pgvector의 HNSW 인덱스에서 코사인 거리가 가장 가까운 사진을 찾습니다.
- 세 갈래 결과를 합칩니다. 사진 속에서 그 글자가 인식된 사진을 맨 앞에, 다음으로 그 지명에서 찍은 사진, 마지막으로 의미가 가장 가까운 사진을 놓습니다.
- 확실히 맞는 것만 돌려줍니다. 가장 가까운 사진과 거의 비슷하게 가까우면서 일반 사진보다 뚜렷이 가까워야 하므로, 느슨하게 관련된 사진까지 돌려주지 않고 약한 일치를 걸러 내는 데 도움이 됩니다.
- 검색어가 분류 이름(풍경, 음식, 스크린샷……)과 같으면 그 분류를 바로 돌려줍니다.
SCRFD로 얼굴과 특징점 5개를 찾고, 특징점에 맞춰 112×112로 잘라 ArcFace로 512차원 특징을 계산합니다(정규화 후 내적이 곧 코사인 유사도). 사진 한 장 처리가 끝날 때마다 점진적으로 분류하므로 전체 계산이 끝나기를 기다릴 필요가 없습니다.
- 충분히 선명하고 정면이며 크기도 충분한 「좋은 얼굴」은 가장 비슷한 얼굴을 찾습니다. 이미 인물에 속한 얼굴이 있으면 다수결로 그 인물에 넣고, 모두 미분류라도 비슷한 얼굴이 3장 이상이면 새 인물을 만듭니다.
- 흐리거나 옆얼굴이거나 너무 작은 얼굴은 새 인물을 만들지 않고, 특정 인물과 아주 비슷할 때만 그 인물에 넣습니다. 한 번 스쳐 간 행인은 등록되지 않습니다.
- 여러 작업이 동시에 분류할 때는 같은 데이터베이스 권고 잠금(advisory lock)을 잡으므로 한 사람이 둘로 나뉘지 않습니다. 대표 사진은 선명하고(라플라시안 분산) 정면이며(yaw) 충분히 큰 단독 얼굴을 고릅니다.
- 같은 장면을 이어 찍은 사진의 CLIP 유사도는 보통 0.94–0.98, 장면이나 자세가 바뀌면 대개 0.7–0.89입니다. 0.93을 유사도 임곗값으로 두면 연사일 가능성이 높은 사진을 찾는 데 도움이 됩니다.
- 추천으로 남길 한 장: 얼굴이 있으면 얼굴이 선명하고 눈을 뜬 채 카메라를 보는 사진, 얼굴이 없으면 파일 크기로 판단합니다(같은 카메라, 같은 설정이면 흔들린 사진은 디테일이 적어 압축 후 크기가 작습니다).
- 같은 파일이 여러 벌 저장된 경우는 「중복」으로 따로 모으므로 고를 필요가 없습니다. 「보관」을 누르면 나머지는 최근 삭제로 옮겨지며, 영구 삭제하기 전까지 복원할 수 있습니다.
모델을 따로 돌리지 않고 CLIP 벡터를 재사용합니다. 「사진 작품」을 묘사한 문장들과 「평범한 일상 사진」을 묘사한 문장들의 평균 벡터를 각각 구해 빼면 「보기 좋은」 방향이 나오고, 사진 벡터를 이 방향에 투영한 값이 점수입니다.
- 순서를 매기는 데만 쓰고 절대 점수로 보지 않습니다. 홈의 추천 캐러셀이 이 점수로 사진을 고릅니다.
- 따로 마련한 문장들로 「잘못 찍힌」 사진(흔들림, 노출 과다 등)을 골라내며, 보기 좋은지와는 별도로 계산합니다.
사용한 기술
모두 성숙한 오픈 소스 구성 요소로, 독점 서비스가 없고 어느 부분이든 대체품이 있습니다.
- GoAPI, 작업 큐, 썸네일 스케줄링
- libvips썸네일과 미리 보기
- FFmpeg동영상 프레임 추출, 변환
- ExifTool사진 메타데이터 읽기·쓰기
- tus이어 올리기 업로드
- Python + FastAPI무상태 HTTP 서비스
- ONNX Runtime얼굴·문자 인식 추론
- PyTorchChinese-CLIP
- InsightFaceSCRFD + ArcFace
- RapidOCRPP-OCR 문자 인식
- PostgreSQL 17업무 데이터와 작업 큐
- pgvector벡터 저장
- HNSW코사인 거리 인덱스
- ip2region로그인 기록의 IP 위치 (오프라인)
- Nominatim역지오코딩 (끌 수 있음)
- React + TypeScript웹 앱
- Vite + Tailwind CSS빌드와 스타일
- MapLibre GL위성 지구본
- Docker Composeamd64 / arm64 이미지
- mDNSwanmory.local LAN 도메인
개인정보와 보안
기본적으로 사진과 AI 결과는 사설 네트워크 안에 머뭅니다. 원격 AI 서비스를 설정하면 미리 보기 이미지가 그 서버로 전송되어 처리됩니다.
AI 서비스는 1440픽셀 미리 보기만 받으며, 파일을 저장하지 않고 데이터베이스, 인물 이름, 원본에도 접근하지 않습니다.
내장 AI 서비스는 Docker 내부 네트워크에만 있어 백엔드만 연결할 수 있습니다. 원격 AI 서비스에 토큰을 설정하면 모든 요청에 Bearer 토큰이 필요합니다.
구성원은 자신이 올린 사진만 삭제할 수 있고 관리자는 전체를 관리합니다. 숨긴 사진은 본인만 볼 수 있으며, 로그인 기록에는 IP로 추정한 대략적인 위치가 표시됩니다.
NAS 폴더를 :ro로 읽기 전용 마운트할 수 있어, 앱이 그 안의 파일을 고치거나 지울 수 없습니다.
모델을 내려받은 뒤에는 인식과 검색이 모두 로컬에서 이루어집니다. 역지오코딩, 위성 지도처럼 인터넷이 필요한 기능은 끌 수 있습니다.
InsightFace 얼굴 모델은 비상업적 용도로만 쓸 수 있습니다. 모델은 이미지에 포함하지 않으며, 내려받기 전에 설정 화면에서 각 모델의 라이선스를 보여 줍니다.