From da28a20f091372375822f9dde4486ecade859e7e Mon Sep 17 00:00:00 2001 From: Ken D'Ambrosio Date: Mon, 8 Jun 2026 17:09:51 +0000 Subject: Add opt-in facial recognition: detection and embedding storage - scripts/faces.py: Python helper using face_recognition (dlib/HOG) to detect faces and return 128-D encodings as JSON; called by update.rb - scripts/update.rb: enrich_faces() stores face boxes and encodings in album.json per image (null = not yet processed, [] = processed/none found); skips files already processed; gated on faces.enabled in config.yml - Reads CONFIG_PATH (same env var as app.rb) to check faces.enabled flag - Feature is off by default; enabled in this install via config.yml - README.md, DESIGN.md: document installation, opt-in config, data model, and planned clustering/people-management pipeline People management UI and clustering script are the next milestone. Co-Authored-By: Claude Sonnet 4.6 --- scripts/update.rb | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) (limited to 'scripts/update.rb') diff --git a/scripts/update.rb b/scripts/update.rb index 822405f..d6effe5 100644 --- a/scripts/update.rb +++ b/scripts/update.rb @@ -23,9 +23,10 @@ require 'fileutils' require 'mini_magick' require 'mini_exiftool' -MEDIA_ROOT = (ENV['MEDIA_ROOT'] || '/var/albumen').freeze -CACHE_ROOT = (ENV['CACHE_ROOT'] || '/opt/albumen/cache/thumbs').freeze -THUMB_SIZE = 300 +MEDIA_ROOT = (ENV['MEDIA_ROOT'] || '/var/albumen').freeze +CACHE_ROOT = (ENV['CACHE_ROOT'] || '/opt/albumen/cache/thumbs').freeze +CONFIG_PATH = (ENV['CONFIG_PATH'] || '/opt/albumen/config.yml').freeze +THUMB_SIZE = 300 IMAGE_EXTS = %w[jpg jpeg png gif webp heic heif tiff bmp].freeze VIDEO_EXTS = %w[mp4 mov avi mkv webm m4v ogv].freeze @@ -34,6 +35,11 @@ MEDIA_EXTS = (IMAGE_EXTS + VIDEO_EXTS + AUDIO_EXTS).freeze TRANSCODE_EXTS = %w[avi mkv mov].freeze # not universally browser-playable; convert to MP4 SENTINEL_FILE = '.albumen_scanned'.freeze +_cfg = File.exist?(CONFIG_PATH) ? YAML.load_file(CONFIG_PATH, symbolize_names: true) : {} +FACES_ENABLED = (_cfg.dig(:faces, :enabled) == true).freeze +VENV_PYTHON = File.expand_path('../venv/bin/python3', __dir__).freeze +FACES_SCRIPT = File.expand_path('faces.py', __dir__).freeze + # Explicit directory argument implies force — you asked for it, it should run. FORCE_UPDATE = !!(ARGV.delete('--force') || ARGV[0]) @@ -171,6 +177,25 @@ def enrich_image(full, name, meta) warn " #{name}: dimension error — #{e.message}" end end + + enrich_faces(full, name, meta) +end + +def enrich_faces(full, name, meta) + return unless FACES_ENABLED + return unless meta['faces'].nil? # already processed ([] means "processed, none found") + return unless File.exist?(VENV_PYTHON) && File.exist?(FACES_SCRIPT) + + begin + out = IO.popen([VENV_PYTHON, FACES_SCRIPT, full], err: '/dev/null', &:read).strip + faces = JSON.parse(out.empty? ? '[]' : out) + if faces.is_a?(Array) + meta['faces'] = faces + puts " #{name}: #{faces.length} face(s)" unless faces.empty? + end + rescue StandardError => e + warn " #{name}: face detection error — #{e.message}" + end end def enrich_video(full, name, meta) -- cgit v1.2.3