From 01f52565f460a0107679999588b73b770f01a98c Mon Sep 17 00:00:00 2001 From: Ken D'Ambrosio Date: Mon, 8 Jun 2026 19:00:02 +0000 Subject: Add people/face clustering feature - scripts/cluster_faces.py: greedy centroid clustering (numpy) with 3 refinement passes; preserves existing UUID/name mappings across re-runs; writes MEDIA_ROOT/people.json atomically. - app.rb: GET /face/* serves cropped+padded face thumbnails (100x100, cached under cache/faces/); GET|POST /admin/people for cluster management; POST /admin/people/recluster runs cluster_faces.py as a background job; POST /admin/people/:uuid saves names+slugs; GET /people public grid of named people; GET /people/:slug photos for one person. - views/admin/people.erb: lists all clusters (named first, then by size), face crop samples, inline name form, re-cluster button with live log. - views/people.erb: public grid of named people. - views/person.erb: photo grid for one person, linking back to album lightbox for each photo. - views/layout.erb: People link in nav (conditional on FACES_ENABLED). - public/css/style.css: styles for people admin list and public tiles. Co-Authored-By: Claude Sonnet 4.6 --- views/admin/people.erb | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 views/admin/people.erb (limited to 'views/admin') diff --git a/views/admin/people.erb b/views/admin/people.erb new file mode 100644 index 0000000..6b7a4d0 --- /dev/null +++ b/views/admin/people.erb @@ -0,0 +1,111 @@ +
+
+ ← Albums + <% unless @clusters.empty? %> + Public Page ↗ + <% end %> +
+ +

People

+ +
+

Clustering

+

+ <%= @total %> cluster<%= @total == 1 ? '' : 's' %> · + <%= @named_count %> named + <% if @updated_at %>· last run <%= @updated_at %><% end %> +

+

+ Re-clustering reads all faces.json files and groups similar faces together. + Existing names are preserved. Run again whenever the face daemon has processed a significant batch of new photos. +

+ + +
+ + <% if @clusters.empty? %> +

+ No face data yet — the face daemon is still processing, or run Re-cluster once it has finished a pass. +

+ <% else %> +
+ <% @clusters.each do |c| %> +
+
+ <% c[:samples].each do |s| %> + + <% end %> + <% if c[:count] > c[:samples].length %> + +<%= c[:count] - c[:samples].length %> + <% end %> +
+
+ <%= c[:count] %> photo<%= c[:count] == 1 ? '' : 's' %> +
+ + + <% if c[:slug] %> + + <% end %> +
+
+
+ <% end %> + <% if @total > @clusters.length %> +

+ Showing <%= @clusters.length %> of <%= @total %> clusters (named first, then largest unnamed). +

+ <% end %> +
+ <% end %> +
+ + -- cgit v1.2.3