wangihong/k-curator
<p align="center"> <a href="https://wangihong-k-curator.hf.space"> <img src="./assets/sai-banner.svg?v=3" alt="사이 SAI — 작품과 당신 사이를 잇다" width="100%"/> </a> </p>
<h1 align="center">사이 (SAI) · 작품과 당신 사이를 잇다</h1>
<p align="center"> 국립중앙박물관 큐레이터 해설을 기반으로,<br/> <b>박물관 가기 전엔 코스 추천, 가서는 작품 해설, 다녀와선 매일 다른 큐레이션</b>을 보여주는<br/> 풀스택 RAG·멀티모달 사이드 프로젝트. </p>
<p align="center"> <a href="https://wangihong-k-curator.hf.space"><img alt="Live Demo" src="https://img.shields.io/badge/▶-Live%20Demo-a0301a?style=for-the-badge"/></a> <a href="./docs/"><img alt="Docs" src="https://img.shields.io/badge/📚-9%20docs-3d362b?style=for-the-badge"/></a> <a href="./LICENSE"><img alt="License" src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge"/></a> <a href="https://github.com/dhksrlghd/sai-museum-docent/actions/workflows/build.yml"><img alt="Build" src="https://github.com/dhksrlghd/sai-museum-docent/actions/workflows/build.yml/badge.svg"/></a> </p>
<p align="center"> <img alt="Python" src="https://img.shields.io/badge/Python-3.12-3776AB?logo=python&logoColor=white"/> <img alt="FastAPI" src="https://img.shields.io/badge/FastAPI-009688?logo=fastapi&logoColor=white"/> <img alt="React" src="https://img.shields.io/badge/React-19-61DAFB?logo=react&logoColor=white"/> <img alt="Vite" src="https://img.shields.io/badge/Vite-8-646CFF?logo=vite&logoColor=white"/> <img alt="Tailwind" src="https://img.shields.io/badge/Tailwind-v4-06B6D4?logo=tailwindcss&logoColor=white"/> <img alt="OpenAI" src="https://img.shields.io/badge/OpenAI-gpt--4o--mini-412991?logo=openai&logoColor=white"/> <img alt="Chroma" src="https://img.shields.io/badge/Chroma-1.5-1a1a1a"/> <img alt="CLIP" src="https://img.shields.io/badge/CLIP-ViT--B%2F32-005CC5"/> <img alt="e5-small" src="https://img.shields.io/badge/e5--small-multilingual-FFD43B"/> <img alt="HF Spaces" src="https://img.shields.io/badge/🤗-Hugging%20Face%20Spaces-FFD21E"/> </p>
<p align="center"> 이름 <b>사이</b>는 "사이(in-between)"의 사이. 작품과 당신 사이, 큐레이터의 시선과 관람객 사이, 어제와 오늘 사이를 잇는다는 뜻. </p>
미리 보기
라이브 사이트에서 캡처한 핵심 화면들. 클릭하면 해당 페이지로 이동.
<p align="center"> <em>모바일 반응형 — 햄버거 메뉴 · stack 레이아웃</em><br/> <a href="https://wangihong-k-curator.hf.space/"><img src="./docs/images/mobile.png" alt="모바일" width="240"/></a> </p>
무엇이 차별화되는가
대부분의 RAG 데모가 "검색 + LLM 응답" 한 화면짜리에서 끝나는 반면, 사이는 데이터를 세 갈래로 풀어 실제 관람객 여정에 매핑했어요:
페이지 / 엔드포인트
프론트엔드 백엔드 API
───────────── ─────────────
/ GET /api/today # 오늘의 테마 + 추천 6점
/plan POST /api/plan # SSE 스트리밍 코스 빌더
/exhibitions GET /api/exhibitions # 7관 36실 + 특별전
/browse GET /api/works # 321점 카탈로그
/work/:id GET /api/works/{id}
GET /api/works/{id}/similar # CLIP 이미지 유사도
/ask POST /api/chat # SSE 3-mode RAG데이터 풍경
기술 스택
- Frontend: React 19 · React Router 6 · Vite 8 · Tailwind CSS v4
- Backend: FastAPI · uvicorn · sse-starlette
- 검색:
- 텍스트: Chroma (
kcurator_relics, cosine) +intfloat/multilingual-e5-small - 이미지: Chroma (
kcurator_images, cosine) +sentence-transformers/clip-ViT-B-32 - LLM: OpenAI
gpt-4o-mini(스트리밍, RAG 그라운딩, hallucination 가드) - 배포: Hugging Face Space (Docker SDK 단일 컨테이너 — 프론트 정적 파일을 FastAPI가 함께 서빙)
데이터 파이프라인
1) scrape_list.py → 321점 ID/제목/썸네일 (단일 요청)
2) scrape_all.py → 321점 본문/메타/이미지 (1.5초 sleep)
3) scrape_permanent.py → 7관 36실 + 643점 + 실 소개
4) scrape_special.py → 진행중 특별전 5건 + 본문
5) match_locations.py → 추천 ↔ 상설 작품 fuzzy match → 위치 메타 보강
6) build_index.py → 텍스트 청킹 + e5-small 임베딩 → Chroma
7) embed_images.py → CLIP 이미지 임베딩 → ChromaDockerfile이 빌드 시점에 (6) (7) 단계를 자동 실행하여 모델·인덱스를 이미지에 베이크합니다. 콜드 스타트 시 모델 다운로드 없이 바로 라이브.
로컬 실행
# 1) Python venv 활성화
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
# 2) .env 작성
# EMUSEUM_API_KEY=... (스크래핑 단계만)
# OPENAI_API_KEY=sk-... (RAG 단계 필수)
# 3) 데이터가 없다면 빌드 (약 25분)
python src/scrape_list.py
python src/scrape_all.py # ~13분
python src/scrape_permanent.py # ~1분
python src/scrape_special.py # ~10초
python src/match_locations.py # 즉시
python src/build_index.py # ~4분
python src/embed_images.py # ~7분
# 4) 백엔드
cd src
python -m uvicorn api:app --reload --port 8000
# 5) 프론트 (다른 터미널)
cd frontend
npm install
npm run dev # http://localhost:5173Hugging Face Space 배포
git remote add hf https://huggingface.co/spaces/<USERNAME>/k-curator
git push hf mainSpace의 Settings → Variables and secrets 에 OPENAI_API_KEY 등록 필수. 첫 빌드는 모델 다운(120MB e5 + 600MB CLIP) + 894장 이미지 임베딩 때문에 15~20분. 이후 push는 8~10분.
구조
src/
api.py FastAPI 진입점 — RAG + Plan + Today + Similar + SPA fallback
rag.py 검색 → 컨텍스트 → LLM 호출 (CLI도 가능)
daily_pick.py 30개 테마 풀 + 결정적 회전
build_index.py 텍스트 청킹·임베딩·Chroma
embed_images.py CLIP 이미지 임베딩·Chroma
search.py CLI 검색 도구 (디버그)
scrape_*.py museum.go.kr 스크래퍼 4종
match_locations.py 추천 ↔ 상설 작품 매칭
frontend/
src/pages/ Home / Plan / Exhibitions / Browse / Work / Ask
src/components/ Header (모바일 햄버거 포함) / Footer / WorkCard / AskBox
src/lib/api.js 백엔드 호출 + SSE 파서
data/
raw/ 321 작품 JSON · 36 실 · 5 특별전 · 위치 매칭
chroma/ 영구 벡터 인덱스 (텍스트 + 이미지 컬렉션)
processed/ chunks.jsonl 사람용 덤프더 깊이 보기 — docs/
출처 / 라이선스
원자료 출처:
사이(SAI) 코드 자체는 포트폴리오 데모이며, 국립중앙박물관/큐레이터의 공식 도슨트가 아닙니다.
Contributing
CONTRIBUTING.md 참고. 이슈/PR/제안 환영.
변경 이력
CHANGELOG.md — v0.0(데이터 탐색) → v2.4(리브랜딩) 까지 마일스톤별 정리.
