Add NextGen library: index DB, thumbnails, AI culling, and CLIP search

Builds the "indexed library" foundation and first intelligent features on
top of the organizer (sql.js index, non-destructive in-place indexing).

Phase 0 — Library index:
- sql.js (WASM SQLite) index DB; contentHash-keyed assets, resumable indexing
  (skip by path+mtime), batch persistence (chosen over native better-sqlite3
  which fails to build on Node 24 / Python 3.12)
- Library folders (in place, non-destructive) + background indexer w/ progress
- Thumbnails generated in the AI worker (canvas->webp), cached in userData;
  served via photoai-media://thumb by hash; thumbnail grid w/ pagination

Phase 1 — AI quality assessment & culling:
- Focus (Laplacian variance), exposure (histogram), eyes-open (face-api EAR)
  computed in one analyze pass alongside the thumbnail
- Culling filters (candidate/rejected) + quality badges
- Adjustable thresholds (live SQL re-classification from stored raw scores,
  no re-analysis) + manual star rating (0-5) and color labels (usermeta)

Phase 2 — CLIP natural-language / similarity search:
- @huggingface/transformers (WASM/WebGPU, no native build)
- CLIP image/text embeddings (lazy-loaded); Korean queries auto-translated
  via opus-mt-ko-en into the English CLIP
- Embeddings stored as SQLite BLOBs; "build search index" batch w/ progress;
  brute-force cosine search; new Search tab
- Note: models download from HF Hub on first use; fully-offline ORT-wasm
  packaging and KO search-accuracy tuning are follow-ups

Tabs added (Organize / Library / Search). All typecheck/tests(12)/build green;
boot smoke verified across phases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 17:32:51 +09:00
parent 6dce580846
commit 72c41ae834
33 changed files with 3136 additions and 358 deletions
+69
View File
@@ -141,6 +141,75 @@ export const MESSAGES: Table = {
'dur.ms': { ko: '{m}분 {s}초', en: '{m}m {s}s' },
'dur.s': { ko: '{s}초', en: '{s}s' },
// 내비게이션 / 라이브러리 (Phase 0)
'nav.organize': { ko: '정리', en: 'Organize' },
'nav.library': { ko: '라이브러리', en: 'Library' },
'nav.search': { ko: '검색', en: 'Search' },
// 검색 (Phase 2)
'search.section': { ko: '검색 색인', en: 'Search index' },
'search.hint': {
ko: '자연어로 사진을 검색하려면 먼저 CLIP 임베딩 색인을 생성하세요(최초 1회 모델 다운로드 필요).',
en: 'Build the CLIP embedding index to search photos by natural language (first run downloads the model).'
},
'search.build': { ko: '검색 색인 생성', en: 'Build search index' },
'search.cancel': { ko: '취소', en: 'Cancel' },
'search.building': { ko: '임베딩 중…', en: 'Embedding…' },
'search.status': { ko: '임베딩 {embedded} / {total}', en: 'Embedded {embedded} / {total}' },
'search.placeholder': {
ko: '예: 푸른 바다, 노을, 강아지가 있는 사진…',
en: 'e.g. blue ocean, sunset, photos with a dog…'
},
'search.go': { ko: '검색', en: 'Search' },
'search.searching': { ko: '검색 중…', en: 'Searching…' },
'search.noResults': { ko: '결과가 없습니다.', en: 'No results.' },
'search.prompt': {
ko: '검색어를 입력하세요.',
en: 'Type a query to search.'
},
'lib.section': { ko: '라이브러리 폴더', en: 'Library Folders' },
'lib.hint': {
ko: '색인할 폴더를 추가하세요. 사진은 옮기지 않고 제자리에서 색인됩니다(비파괴).',
en: 'Add folders to index. Photos are indexed in place, never moved (non-destructive).'
},
'lib.add': { ko: '폴더 추가', en: 'Add folder' },
'lib.empty': { ko: '색인할 라이브러리 폴더가 없습니다.', en: 'No library folders yet.' },
'lib.remove': { ko: '제거', en: 'Remove' },
'lib.index': { ko: '색인 시작', en: 'Start indexing' },
'lib.cancel': { ko: '취소', en: 'Cancel' },
'lib.indexing': { ko: '색인 중…', en: 'Indexing…' },
'lib.assets': { ko: '색인된 자산 {n}개', en: '{n} assets indexed' },
'lib.progress': { ko: '{done} / {total} · 신규 {indexed} · 스킵 {skipped}', en: '{done} / {total} · {indexed} new · {skipped} skipped' },
'lib.doneSummary': {
ko: '완료 — 신규 {indexed} · 스킵 {skipped} · 실패 {failed} · 총 {assets}개',
en: 'Done — {indexed} new · {skipped} skipped · {failed} failed · {assets} total'
},
'lib.grid': { ko: '색인된 사진', en: 'Indexed photos' },
'lib.gridEmpty': {
ko: '색인된 사진이 없습니다. 폴더를 추가하고 색인을 실행하세요.',
en: 'No indexed photos. Add a folder and run indexing.'
},
'lib.loadMore': { ko: '더 보기', en: 'Load more' },
// 컬링 필터 / 품질 플래그 (Phase 1)
'cull.all': { ko: '전체', en: 'All' },
'cull.candidate': { ko: '고품질 후보', en: 'Candidates' },
'cull.rejected': { ko: '제외 후보', en: 'Rejected' },
'flag.candidate': { ko: '후보', en: 'Keep' },
'flag.blurry': { ko: '흐림', en: 'Blurry' },
'flag.eyesClosed': { ko: '눈감음', en: 'Eyes closed' },
'flag.badExposure': { ko: '노출', en: 'Exposure' },
'cull.thresholds': { ko: '품질 임계값', en: 'Quality thresholds' },
'cull.focus': { ko: '초점', en: 'Focus' },
'cull.exposure': { ko: '노출', en: 'Exposure' },
'cull.eyes': { ko: '눈 뜸', en: 'Eyes open' },
'cull.thresholdHint': {
ko: '값을 올리면 더 엄격하게 제외됩니다. 변경 즉시 재분석 없이 반영됩니다.',
en: 'Higher = stricter rejection. Applied instantly without re-analysis.'
},
'cull.reset': { ko: '기본값', en: 'Reset' },
'cull.ratingMin': { ko: '별점', en: 'Rating' },
// 메뉴
'menu.file': { ko: '파일', en: 'File' },
'menu.edit': { ko: '편집', en: 'Edit' },