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
+20 -3
View File
@@ -4,6 +4,7 @@ import { extname } from 'node:path'
import { MEDIA_SCHEME } from '@shared/constants'
import { profileStore } from './profileStore'
import { presetStore } from './presetStore'
import { thumbPath } from './thumbnails'
import { logger } from './logger'
const MIME: Record<string, string> = {
@@ -41,12 +42,28 @@ export function registerMediaScheme(): void {
export function handleMediaProtocol(): void {
protocol.handle(MEDIA_SCHEME, async (request) => {
try {
// request.url = photoai-media://img/?p=<encodeURIComponent(절대경로)>
const url = request.url
// 썸네일 요청: photoai-media://thumb/?t=<contentHash>
const tMarker = 't='
const ti = url.indexOf(tMarker)
if (ti >= 0) {
const hash = decodeURIComponent(url.slice(ti + tMarker.length))
// 해시는 16진수만 허용 → 경로 조작 차단
if (!/^[a-f0-9]+$/.test(hash)) return new Response('bad hash', { status: 400 })
const data = await readFile(thumbPath(hash))
return new Response(new Uint8Array(data), {
status: 200,
headers: { 'content-type': 'image/webp', 'cache-control': 'no-cache' }
})
}
// 참조 이미지 요청: photoai-media://img/?p=<encodeURIComponent(절대경로)>
// searchParams의 자동 디코딩과의 이중 디코딩을 피하려고 raw 문자열을 직접 파싱한다.
const marker = 'p='
const i = request.url.indexOf(marker)
const i = url.indexOf(marker)
if (i < 0) return new Response('missing path', { status: 400 })
const filePath = decodeURIComponent(request.url.slice(i + marker.length))
const filePath = decodeURIComponent(url.slice(i + marker.length))
// 등록된 참조 이미지(활성 프로필 또는 프리셋)에 한해 제공 — 임의 파일 읽기 차단
const allowed =