8a8c10248c
Local-first photo organizer that auto-sorts images by face recognition and EXIF capture date. - Electron app with 3-process split: Main (Node) / UI Renderer (React) / hidden Inference Renderer (face-api + WebGL) - Core pipeline: scan -> face match + EXIF -> path build -> atomic move/copy - Move = copy -> verify -> delete; auto-rename on filename collision - 1st-registered profile = move, others = copy; unmatched -> [미정]/YYYY/MM - EXIF date with mtime fallback - Vitest unit tests (pathBuilder / fileOps / concurrency) all green - electron-builder config for Windows (nsis) + macOS (dmg) - Docs: PRD / DECISIONS / ARCHITECTURE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
1.9 KiB
JavaScript
63 lines
1.9 KiB
JavaScript
// face-api 모델 가중치를 ./models 로 내려받는 스크립트.
|
|
// 출처: @vladmandic/face-api 모델 저장소 (오프라인 동작을 위해 앱에 동봉).
|
|
// node scripts/download-models.mjs
|
|
|
|
import { mkdir, writeFile, access } from 'node:fs/promises'
|
|
import { constants } from 'node:fs'
|
|
import { join, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
const MODELS_DIR = join(__dirname, '..', 'models')
|
|
|
|
const BASE = 'https://raw.githubusercontent.com/vladmandic/face-api/master/model'
|
|
|
|
// 필요한 모델: SSD MobileNet v1, Tiny Face Detector, Landmark68, Recognition
|
|
const FILES = [
|
|
'ssd_mobilenetv1_model-weights_manifest.json',
|
|
'ssd_mobilenetv1_model.bin',
|
|
'tiny_face_detector_model-weights_manifest.json',
|
|
'tiny_face_detector_model.bin',
|
|
'face_landmark_68_model-weights_manifest.json',
|
|
'face_landmark_68_model.bin',
|
|
'face_recognition_model-weights_manifest.json',
|
|
'face_recognition_model.bin'
|
|
]
|
|
|
|
async function exists(p) {
|
|
try {
|
|
await access(p, constants.F_OK)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function download(file) {
|
|
const dest = join(MODELS_DIR, file)
|
|
if (await exists(dest)) {
|
|
console.log(` skip ${file} (이미 존재)`)
|
|
return
|
|
}
|
|
const url = `${BASE}/${file}`
|
|
const res = await fetch(url)
|
|
if (!res.ok) throw new Error(`다운로드 실패 ${res.status}: ${url}`)
|
|
const buf = Buffer.from(await res.arrayBuffer())
|
|
await writeFile(dest, buf)
|
|
console.log(` ok ${file} (${(buf.length / 1024).toFixed(0)} KB)`)
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(MODELS_DIR, { recursive: true })
|
|
console.log(`모델 다운로드 → ${MODELS_DIR}`)
|
|
for (const f of FILES) {
|
|
await download(f)
|
|
}
|
|
console.log('완료. 모델 준비됨.')
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('오류:', err.message)
|
|
process.exit(1)
|
|
})
|