Facet Photo Analysis and Culling Engine
repository·master·Indexed 41 days ago
https://github.com/ncoevoet/facetA local-first, AI-powered photo analysis engine that uses computer vision and face recognition to score, rank, and cull photo libraries. It analyzes images across 9 dimensions—including aesthetic quality and technical sharpness—and provides a web gallery for browsing, semantic search, and organization. Supports JPEG, HEIF/HEIC, and various RAW formats. Built with Python 3.10+, FastAPI 0.115+, and Angular 21.
What's inside Facet
- Facet is a multi-dimensional photo analysis engine designed to score, rank, and cull local photo libraries. It provides a gallery interface for browsing analyzed photos. The engine supports various workflows including scoring, culling, browsing, organizing, and understanding photo content.
Overview of Facet
masterFacet is a local photo-analysis and culling engine designed to run entirely on your machine without requiring cloud services, accounts, or API keys. It analyzes images across 9 dimensions (such as aesthetic quality and face sharpness) to provide scoring, browsing, culling, and organization capabilities via a web gallery.
Key Technical Specifications:
- Python Version: 3.10+
- Frontend: Angular 21
- Backend: FastAPI 0.115+
- Supported Platforms: Windows, macOS, Linux, and Docker
- Hardware Acceleration: GPU is auto-detected and optional. It can run CPU-only or utilize up to 24 GB of VRAM.
Define scoring categories
masterCategories are defined in an array within
scoring_config.json. Each category determines how photos are evaluated and prioritized.Category Structure
Each category object contains:
name: A unique identifier for the category.priority: An integer where lower values indicate higher priority (evaluated first).filters: Conditions used to match photos to this category.weights: Scoring metric weights. These must sum to 100.- Note: Every category's
weightsblock can include five metric keys:symmetry_percent,balance_percent,edge_entropy_percent,fractal_percent, andcolor_harmony_percent. These are populated by running--recompute-form.
- Note: Every category's
modifiers: Behavior adjustments for the category.tags: CLIP vocabulary used for tag-based matching.
Important: Because arrays are replaced wholesale, you must copy the entire default
categoriesarray fromconfig/scoring_config.default.jsonif you wish to modify or add a category.Understand processing modes: auto, multi-pass, and single-pass
masterFacet supports three processing modes to balance performance and hardware constraints:
auto: Automatically selects betweenmulti-passorsingle-passbased on available VRAM.multi-pass: Designed for limited VRAM. It loads images in RAM chunks (defined byram_chunk_size), then sequentially loads a model, processes the chunk, and unloads the model. This allows large models (like tagger/composition VLMs) to run on hardware that couldn't hold all models simultaneously.single-pass: Loads all models into VRAM at once. This is the fastest mode but requires high VRAM capacity.
Metadata interoperability with Lightroom, darktable, and others
masterFacet is designed to complement existing photo workflows (Lightroom, darktable, digiKam, etc.) rather than replace them. It achieves this through XMP metadata interoperability:
- Writing Metadata: Facet writes ratings, labels, keywords, captions, and named face regions to sidecar
.xmpfiles (leaving originals intact by default). It can also embed these into JPEG/HEIC/TIFF/PNG/DNG files using the gallery's "Write metadata to file" action or the CLI flags--export-sidecars --embed-originals. - Reading Metadata: Facet can re-read external changes made in other editors using the
--import-sidecarsflag.
- Writing Metadata: Facet writes ratings, labels, keywords, captions, and named face regions to sidecar
Webhook security and delivery behavior
masterFacet implements several security measures and delivery guarantees for outgoing webhooks:
Security
- URL Validation: Only
httpandhttpsprotocols are allowed. Hostnames resolving to private, loopback, or link-local addresses (including the cloud metadata IP169.254.169.254) are rejected. - DNS Rebinding Protection: The request connects to the resolved IP while maintaining the original hostname in the
Hostheader to prevent redirection to private addresses.
Delivery
- Best-Effort: Delivery is not retried on 5xx errors, timeouts, or transport errors. For at-least-once delivery guarantees, implement queuing and retries on your receiving server.
- Timeout: The outbound request timeout is 10 seconds.
- URL Validation: Only
Understand and optimize scoring metrics
masterFine-tune how your photos are evaluated using these advanced features:
- AI Critique: Get detailed breakdowns of scores and natural language evaluations using a VLM
[GPU][16gb/24gb]. - Weight Adjustment: Use the category weight editor with real-time score previews. Use A/B photo comparisons to help the system learn your preferences.
- Scoring Contexts: Control which category scores a photo (independent of weights). You can reorder global category priority, apply named contexts (e.g., "Portrait Session", "Wildlife") per album, or define photo-specific category overrides.
- 'My Taste' Sorting: Sort the gallery by a score learned from your personal classifier. A confidence badge indicates the learned coverage and precision.
- Learning from Labels: Your selection decisions, star ratings, favorites, and rejections are used to optimize weights. Use the following flags to enable this:
--sync-label-comparisons--mine-insights
- Snapshots: Save, restore, and compare different weight configurations.
- AI Captions
[GPU][16gb/24gb]: Generates editable text descriptions, available in 5 languages.
- AI Critique: Get detailed breakdowns of scores and natural language evaluations using a VLM
Understand the Facet workload separation
masterFacet splits its operations into two distinct workloads to optimize hardware usage:
- Scoring (
facet.py): Requires high resources (GPU with 6-24GB VRAM or CPU with 8GB+ RAM) to analyze and score photos. - Viewer (
viewer.py): A low-resource component that serves the web gallery. This is the only component that must run on your remote server or NAS.
Recommended Workflow: Perform scoring on a powerful workstation, then sync the resulting database to your viewer server.
- Scoring (
Handle normalization convergence with per-category settings
masterWhen
per_categoryis enabled in thenormalizationconfig, a single--recompute-averagerun may not converge if your changes also affect photo categories (e.g., reordering priorities or changing scoring contexts).Why this happens:
compute_percentiles_per_categorygroups photos by their currently storedphotos.categorycolumn. If a config change causes photos to move to new categories, the first recompute uses the old category groupings.Required Workflow:
- Apply configuration changes that affect categories.
- Run
--recompute-averageonce. - Run
--recompute-averagea second time to ensure percentiles are computed against the updatedcategorycolumn.
Configure the event-driven plugin system
masterThe plugin system allows you to react to scoring events via Python modules, webhooks, or built-in actions. Enable it using the
enabledkey in thepluginsblock.Configuration Keys:
enabled: Master switch (Default:false).high_score_threshold: Minimum aggregate score to triggeron_high_scoreevents (Default:8.0).webhooks: A list of webhook endpoints to receive JSON POST payloads.actions: A map of named built-in actions triggered by events.
Supported Events and Payloads:
on_score_complete: Triggered after each photo is scored. Payload includespath,filename,aggregate,aesthetic,comp_score,category, andtags.on_new_photo: Triggered when a photo enters the database. Uses the same payload ason_score_complete.on_high_score: Triggered when aggregate score $\ge$high_score_threshold. Uses the same payload ason_score_complete.on_burst_detected: Triggered when a burst group is identified. Payload includesburst_group_id,photo_count,best_path, andpaths.
{ "plugins": { "enabled": true, "high_score_threshold": 8.0, "webhooks": [ { "url": "https://example.com/hook", "events": ["on_score_complete", "on_high_score"], "min_score": 8.0 } ], "actions": { "copy_high_scores": { "event": "on_high_score", "action": "copy_to_folder", "folder": "/path/to/best-photos", "min_score": 9.0 } } } }How the Facet scoring system works
masterFacet classifies photos into categories and then assigns a score based on that category's specific weights. The process follows five steps:
- Category Detection: The photo is analyzed for content using faces, tags, and EXIF data.
- Filter Evaluation: Categories are evaluated in priority order. The first category whose filters match the photo is selected (this is the scoring context).
- Weight Application: Category-specific weights are applied to various metrics.
- Modifier Application: Bonuses, penalties, and behavior flags are applied.
- Final Score: A weighted sum is calculated and clamped to a range of 0-10.
Note: Reordering categories or using scoring contexts only changes the order in which categories are tried; it cannot force a category to match if its filters (like EXIF data or required tags) are not met.
How Facet evaluates photos
masterFacet evaluates every photo across 9 dimensions: aesthetic quality, composition, face quality, eye sharpness, technical sharpness, color, exposure, subject recognition, and dynamic range.
Photos are categorized into over 30 content categories (e.g., Portrait, Landscape, Macro, Street) and evaluated using category-specific weights. You can use the Beste Auswahl (Best Selection) filter to sort your library based on a combined score.
To see a detailed breakdown of a photo's score and its EXIF data, hover your mouse over any photo to reveal a tooltip.