Initial commit: AI Photo Organizer (Electron + face-api)
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>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// 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)
|
||||
})
|
||||
Reference in New Issue
Block a user