diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/faces.py | 51 | ||||
| -rw-r--r-- | scripts/update.rb | 31 |
2 files changed, 79 insertions, 3 deletions
diff --git a/scripts/faces.py b/scripts/faces.py new file mode 100644 index 0000000..d072376 --- /dev/null +++ b/scripts/faces.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Detect faces in an image and return their bounding boxes and 128-D encodings. + +Usage: python3 faces.py <image_path> + +Stdout: JSON array — one object per face: + [{"box": [top, right, bottom, left], "encoding": [128 floats]}, ...] + +Returns "[]" when no faces are found or the image cannot be opened. +Errors are written to stderr; stdout is always valid JSON. +""" +import sys +import json + + +def main(): + if len(sys.argv) < 2: + print("[]") + return + + path = sys.argv[1] + try: + import face_recognition + except ImportError as e: + print(f"face_recognition not available: {e}", file=sys.stderr) + print("[]") + return + + try: + img = face_recognition.load_image_file(path) + except Exception as e: + print(f"Could not load {path}: {e}", file=sys.stderr) + print("[]") + return + + try: + locations = face_recognition.face_locations(img, model="hog") + encodings = face_recognition.face_encodings(img, locations) + result = [ + {"box": list(loc), "encoding": enc.tolist()} + for loc, enc in zip(locations, encodings) + ] + print(json.dumps(result)) + except Exception as e: + print(f"Detection error for {path}: {e}", file=sys.stderr) + print("[]") + + +if __name__ == "__main__": + main() 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) |
