summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/faces.py51
-rw-r--r--scripts/update.rb31
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)