ARCHITECTURE

아키텍처

핵심 컨테이너 3개에 mDNS 서비스를 더한 구성으로, 별도의 메시지 브로커나 클라우드 서비스가 필요 없습니다. 업로드하거나 NAS 폴더에서 가져온 사진은 메타데이터, 썸네일, 시맨틱 벡터, 얼굴, 문자 순으로 처리되어 자연어·인물·장소로 검색할 수 있는 보관함이 됩니다.

3 + 1개 컨테이너

Go 백엔드, AI 서비스, PostgreSQL, 그리고 LAN 도메인을 알리는 mDNS

5종류의 작업

metadata, thumbnail, clip, face, ocr

512–1024차원 벡터

Chinese-CLIP 이미지·텍스트 벡터(모델 등급에 따라 512·768·1024차원), ArcFace 얼굴 특징은 512차원

HNSW벡터 인덱스

pgvector 코사인 거리 인덱스. 검색과 얼굴 비교를 데이터베이스 안에서 수행

SYSTEM

시스템 구조

브라우저는 Go 백엔드에만 연결합니다. 백엔드가 업로드, 작업 스케줄링, 썸네일, 권한 관리를 맡고, 미리 보기 이미지를 AI 서비스에 넘겨 추론한 뒤 결과를 모두 PostgreSQL에 기록합니다. AI 서비스는 상태가 없어 데이터베이스에 접근하지도 파일을 저장하지도 않으므로, 더 강력한 기기에 따로 배포할 수 있습니다.

클라이언트PC 브라우저웹 앱은 Go 바이너리에 내장모바일 브라우저iPhone · Androidwanmory.localLAN 도메인, IP 외울 필요 없음HTTP :8080DOCKER COMPOSEGo 백엔드 · serverAPI, 업로드, 작업 큐, 썸네일, 권한, 정적 웹REST APItus 이어 올리기작업 큐 Worker계정 · 로그인 기록libvips 썸네일ExifTool 메타데이터FFmpeg 동영상역지오코딩(선택)앨범 · 스마트 앨범연사 · 유사AI 추천 · 그날의 오늘폴더 스캔 15분mdnswanmory.local 알림AI 서비스 · mlFastAPI + ONNX Runtime / PyTorch, 무상태SCRFD 얼굴 검출ArcFace 얼굴 특징Chinese-CLIPPP-OCR 문자 인식PostgreSQL 17 + pgvector업무 데이터, 작업 큐, 벡터를 한 DB에. Redis 불필요assetsfaces · peoplejobs 큐settingsHNSW 인덱스SQL/predict · 1440px 미리 보기저장소 · 확장/data업로드한 원본, 썸네일, 1440px 미리 보기/photos/* (읽기 전용 :ro 가능)NAS의 기존 사진 폴더, 여러 개 마운트 가능./models모델 파일, 웹 설정에서 다운로드원격 AI 서비스 (선택)Mac (CoreML / MPS) · NVIDIA CUDA토큰 인증, 1440px 미리 보기만 받고 원본은 건드리지 않음읽기·쓰기스캔 · 읽기 전용로드HTTP + 토큰
백엔드는 Go 프로그램 하나뿐

웹 앱은 빌드되어 프로그램에 내장되므로 배포에 Node가 필요 없습니다. 시작할 때 데이터베이스 마이그레이션이 자동으로 실행되며, 업그레이드는 이미지만 바꾸면 됩니다.

AI 서비스를 다른 곳에 배포

내장 AI 서비스는 Docker 내부 네트워크에서 CPU로 추론합니다. 웹에서 원격 주소를 입력하면 집에 있는 그래픽 카드 PC나 Mac으로 전환할 수 있습니다.

데이터베이스 하나에 모든 데이터를

사진 정보, 인물, 설정, 작업 큐, 벡터를 모두 PostgreSQL에 저장하므로 업무 데이터와 작업 상태는 함께 백업할 수 있습니다. 사진 파일은 따로 백업해야 합니다.

PIPELINE

사진 처리 파이프라인

사진을 가져오면 먼저 메타데이터를 추출하고 썸네일을 만든 뒤, 시맨틱 벡터 계산과 얼굴·문자 인식을 병렬로 진행합니다. 각 단계는 따로 큐에 들어가므로 재시작하거나 AI 서비스가 끊겨도 작업이 사라지지 않고, 복구되면 이어서 처리합니다.

01ingest
가져오기
  • 웹 업로드는 tus 사용, 끊겨도 이어 올리기
  • /photos 폴더를 15분마다 스캔
  • SHA-1 체크섬을 계산해 assets에 기록
백엔드
02metadata
메타데이터
  • ExifTool로 크기와 촬영 시각 읽기
  • GPS, 카메라, 렌즈, 조리개와 셔터
  • 시간대가 없으면 설정한 시간대로 계산
백엔드
03thumbnail
썸네일
  • libvips로 썸네일 생성
  • 이어서 1440px 미리 보기 생성
  • 동영상은 FFmpeg로 프레임 추출·변환
백엔드
04clip
시맨틱 벡터
  • Chinese-CLIP으로 1024차원 벡터 계산
  • 자연어 검색, 자동 분류
  • 미적 점수, 비슷한 사진
AI · 병렬
05face
얼굴
  • SCRFD로 얼굴 검출, 특징점 5개로 정렬
  • ArcFace로 512차원 특징 계산
  • 인물로 점진적 분류
AI · 병렬
06ocr
문자
  • PP-OCR로 사진 속 글자 인식
  • 글자로 사진 검색
  • 신분증, 문서, 스크린샷 판별
AI · 병렬
SCHEDULING

백그라운드 작업 스케줄링

별도의 메시지 브로커는 없습니다. 작업 큐는 PostgreSQL의 jobs 테이블이며, 사진 데이터와 같은 트랜잭션으로 기록되어 사진이 저장되면 작업도 반드시 큐에 들어갑니다. 워커는 SKIP LOCKED로 작업을 가져가고, 임대 기간으로 멈춤을 막으며, 실패하면 제곱 백오프로 재시도합니다.

pending대기 중running워커가 가져감done완료failed횟수 소진SKIP LOCKED로 가져가기성공재시도 횟수 소진실패: n² × 30초 후 재시도AI 연결 불가: 15초 후 재시도, 실패로 세지 않음임대 10분: 시간 내 결과가 없으면 30초마다 회수해 pending으로 되돌림
jobs/worker.go · claim
-- 작업 하나 가져오기 (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 SCHEDULING

AI 스케줄링

AI 서비스는 미리 보기 이미지를 받아 얼굴, 벡터, 문자를 돌려주는 일만 합니다. 서비스 주소, 모델, 동시 처리 수는 백엔드가 정하고, AI 서비스는 하드웨어에 따라 CUDA, CoreML / MPS, OpenVINO, CPU 중에서 고릅니다.

AI 작업 큐clip · face · ocr동시 처리 수는 설정대로, 흐름 제어 조절 가능Go 백엔드 · AI 클라이언트서비스·등급 선택, model_ids 확인· 15초마다 /health 확인· 연결 불가 → 작업은 큐에서 대기· 등급 자동 → 하드웨어로 추천가져가기내장 AI 서비스Docker 내부 네트워크 · CPU원격 AI 서비스집의 Mac · 그래픽 카드 PC원격 워커 노드직접 작업 가져가기 (계획 중)둘 중 하나CPUONNX RuntimeCoreML / MPS · CUDA쓸 수 있는 가속 자동 선택OpenVINOIntel 내장 그래픽
3단계 모델 조합

「자동」을 선택하면 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급 NASSCRFD-10G + ArcFace R100 (antelopev2)Chinese-CLIP ViT-L/14 · 768차원
lite · 경량N100, 라즈베리 파이 5SCRFD + ArcFace R50 (buffalo_l)Chinese-CLIP ViT-B/16 · 512차원
AI 서비스 API · /predict
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"}
}
ALGORITHMS

핵심 알고리즘

검색, 인물, 연사, AI 추천 뒤에서 쓰이는 모델, 알고리즘, 임곗값입니다. AI 모델에 기대는 것도 있고, 벡터 계산과 규칙으로 해결하는 것도 있습니다.

자연어 검색

Chinese-CLIP은 사진과 글을 같은 벡터 공간에 놓습니다. 사진을 가져올 때 이미지 벡터를 미리 계산해 두고, 검색할 때는 「아기 세 돌 생일」 같은 문장을 텍스트 벡터로 바꿔 pgvector의 HNSW 인덱스에서 코사인 거리가 가장 가까운 사진을 찾습니다.

sim(q, p) = q · p / (‖q‖ ‖p‖)
  • 세 갈래 결과를 합칩니다. 사진 속에서 그 글자가 인식된 사진을 맨 앞에, 다음으로 그 지명에서 찍은 사진, 마지막으로 의미가 가장 가까운 사진을 놓습니다.
  • 확실히 맞는 것만 돌려줍니다. 가장 가까운 사진과 거의 비슷하게 가까우면서 일반 사진보다 뚜렷이 가까워야 하므로, 느슨하게 관련된 사진까지 돌려주지 않고 약한 일치를 걸러 내는 데 도움이 됩니다.
  • 검색어가 분류 이름(풍경, 음식, 스크린샷……)과 같으면 그 분류를 바로 돌려줍니다.
얼굴 분류

SCRFD로 얼굴과 특징점 5개를 찾고, 특징점에 맞춰 112×112로 잘라 ArcFace로 512차원 특징을 계산합니다(정규화 후 내적이 곧 코사인 유사도). 사진 한 장 처리가 끝날 때마다 점진적으로 분류하므로 전체 계산이 끝나기를 기다릴 필요가 없습니다.

d(a, b) = 1 − a · b  좋은 얼굴 d < 0.55 · 흐린·옆얼굴 d < 0.45
  • 충분히 선명하고 정면이며 크기도 충분한 「좋은 얼굴」은 가장 비슷한 얼굴을 찾습니다. 이미 인물에 속한 얼굴이 있으면 다수결로 그 인물에 넣고, 모두 미분류라도 비슷한 얼굴이 3장 이상이면 새 인물을 만듭니다.
  • 흐리거나 옆얼굴이거나 너무 작은 얼굴은 새 인물을 만들지 않고, 특정 인물과 아주 비슷할 때만 그 인물에 넣습니다. 한 번 스쳐 간 행인은 등록되지 않습니다.
  • 여러 작업이 동시에 분류할 때는 같은 데이터베이스 권고 잠금(advisory lock)을 잡으므로 한 사람이 둘로 나뉘지 않습니다. 대표 사진은 선명하고(라플라시안 분산) 정면이며(yaw) 충분히 큰 단독 얼굴을 고릅니다.
연사와 비슷한 사진
같은 카메라 Δt ≤ 2s 또는 Δt ≤ 60s 이고 sim ≥ 0.93
  • 같은 장면을 이어 찍은 사진의 CLIP 유사도는 보통 0.94–0.98, 장면이나 자세가 바뀌면 대개 0.7–0.89입니다. 0.93을 유사도 임곗값으로 두면 연사일 가능성이 높은 사진을 찾는 데 도움이 됩니다.
  • 추천으로 남길 한 장: 얼굴이 있으면 얼굴이 선명하고 눈을 뜬 채 카메라를 보는 사진, 얼굴이 없으면 파일 크기로 판단합니다(같은 카메라, 같은 설정이면 흔들린 사진은 디테일이 적어 압축 후 크기가 작습니다).
  • 같은 파일이 여러 벌 저장된 경우는 「중복」으로 따로 모으므로 고를 필요가 없습니다. 「보관」을 누르면 나머지는 최근 삭제로 옮겨지며, 영구 삭제하기 전까지 복원할 수 있습니다.
AI 추천의 미적 점수

모델을 따로 돌리지 않고 CLIP 벡터를 재사용합니다. 「사진 작품」을 묘사한 문장들과 「평범한 일상 사진」을 묘사한 문장들의 평균 벡터를 각각 구해 빼면 「보기 좋은」 방향이 나오고, 사진 벡터를 이 방향에 투영한 값이 점수입니다.

score(p) = p · (μ작품 − μ일상)
  • 순서를 매기는 데만 쓰고 절대 점수로 보지 않습니다. 홈의 추천 캐러셀이 이 점수로 사진을 고릅니다.
  • 따로 마련한 문장들로 「잘못 찍힌」 사진(흔들림, 노출 과다 등)을 골라내며, 보기 좋은지와는 별도로 계산합니다.
TECH STACK

사용한 기술

모두 성숙한 오픈 소스 구성 요소로, 독점 서비스가 없고 어느 부분이든 대체품이 있습니다.

백엔드
  • Go
    API, 작업 큐, 썸네일 스케줄링
  • libvips
    썸네일과 미리 보기
  • FFmpeg
    동영상 프레임 추출, 변환
  • ExifTool
    사진 메타데이터 읽기·쓰기
  • tus
    이어 올리기 업로드
AI 서비스
  • Python + FastAPI
    무상태 HTTP 서비스
  • ONNX Runtime
    얼굴·문자 인식 추론
  • PyTorch
    Chinese-CLIP
  • InsightFace
    SCRFD + ArcFace
  • RapidOCR
    PP-OCR 문자 인식
데이터
  • PostgreSQL 17
    업무 데이터와 작업 큐
  • pgvector
    벡터 저장
  • HNSW
    코사인 거리 인덱스
  • ip2region
    로그인 기록의 IP 위치 (오프라인)
  • Nominatim
    역지오코딩 (끌 수 있음)
프런트엔드와 배포
  • React + TypeScript
    웹 앱
  • Vite + Tailwind CSS
    빌드와 스타일
  • MapLibre GL
    위성 지구본
  • Docker Compose
    amd64 / arm64 이미지
  • mDNS
    wanmory.local LAN 도메인
PRIVACY

개인정보와 보안

기본적으로 사진과 AI 결과는 사설 네트워크 안에 머뭅니다. 원격 AI 서비스를 설정하면 미리 보기 이미지가 그 서버로 전송되어 처리됩니다.

최소한의 데이터

AI 서비스는 1440픽셀 미리 보기만 받으며, 파일을 저장하지 않고 데이터베이스, 인물 이름, 원본에도 접근하지 않습니다.

인증

내장 AI 서비스는 Docker 내부 네트워크에만 있어 백엔드만 연결할 수 있습니다. 원격 AI 서비스에 토큰을 설정하면 모든 요청에 Bearer 토큰이 필요합니다.

권한

구성원은 자신이 올린 사진만 삭제할 수 있고 관리자는 전체를 관리합니다. 숨긴 사진은 본인만 볼 수 있으며, 로그인 기록에는 IP로 추정한 대략적인 위치가 표시됩니다.

원본은 읽기 전용

NAS 폴더를 :ro로 읽기 전용 마운트할 수 있어, 앱이 그 안의 파일을 고치거나 지울 수 없습니다.

인터넷 없이도 사용

모델을 내려받은 뒤에는 인식과 검색이 모두 로컬에서 이루어집니다. 역지오코딩, 위성 지도처럼 인터넷이 필요한 기능은 끌 수 있습니다.

모델 라이선스

InsightFace 얼굴 모델은 비상업적 용도로만 쓸 수 있습니다. 모델은 이미지에 포함하지 않으며, 내려받기 전에 설정 화면에서 각 모델의 라이선스를 보여 줍니다.