| Age | Commit message (Collapse) | Author | Files | Lines |
|
People bulk-move previously required moving members into an existing
person, the pool, or the blacklist; this adds a "new person" target
that creates a fresh cluster from the selected faces.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Mirrors the browse page filter: files with transcoded_to set are hidden
from unprivileged users, so the album video badge now matches what they
actually see.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Replace media_count helper with media_counts returning {photos, videos}.
Album overview shows photo count as before, plus a coloured ▶ N badge
only when an album (or its sub-albums) contains videos.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Firefox has no HEVC support. Pixel 9a (and newer phones) record video
in HEVC by default. update.rb now probes the codec of every .mp4/.m4v
file via ffprobe and transcodes non-web-friendly codecs (anything not
h264/vp8/vp9/av1) to H.264 MP4 with a .h264.mp4 suffix, same as the
existing MOV→MP4 flow. The original is kept and hidden from non-admins.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When ?photo= is in the URL, @og_use_media was forcing the full-res
image (2-3MB) as the og:image. WhatsApp fetched it but showed no
preview, likely due to size or timeout. Now uses the thumbnail.
Also removed hardcoded og:image:width/height (1200x630) from individual
photo/album pages — those dimensions only apply to the default fallback
image and were causing WhatsApp to reject previews when the actual image
dimensions didn't match the declared ones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
nginx was sending X-Forwarded-Proto: http (its own scheme) rather than
https (the edge scheme), so og:url and og:image were http:// URLs.
Fixed by hardcoding https in the nginx proxy_set_header since HTTPS is
always terminated at the upstream Apache router.
Added a 1200x630 landscape og-default.png (WhatsApp requires ~1.91:1
ratio) and explicit og:image:width/height meta tags. Also tracks the
nginx site config in the repo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
params['entries[]']
Rack::Utils.parse_nested_query maps form fields named 'entries[]' to
params['entries'] (without brackets). The route was reading
params['entries[]'] which was always nil, making entries always empty
and triggering the halt 400 guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The overlaid checkbox had pointer-events:none so clicks fell through to
the photo link. Replaced with a real <input type=checkbox> rendered
below each face crop. Checkbox change events drive selection state;
clicking anywhere on the card except the photo link also toggles it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The bulk selection panel now lives on /admin/people/:uuid — the face
crop grid page — which is what was actually requested. A sticky left
panel shows the cluster name, the name form, a selection counter, and
bulk action controls. Clicking a face crop toggles selection; clicking
the photo link still opens the album. Bulk actions: move selected faces
to a named person, move to pool, or blacklist. The per-face individual
dropdowns are replaced by the panel. Merge-entire-cluster and
Blacklist-cluster moved to collapsible/button in the panel too.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
On /people/:slug, admins see a sticky left panel and selectable photo
tiles. Clicking a tile (or its checkbox overlay) toggles selection;
clicking without modifier still opens the photo in a new tab. The panel
shows the selection count and two actions:
- Reassign to person: moves the selected photos' face entries from the
current person's cluster to the chosen person.
- Move to album: moves the photo files on disk and updates album.json,
faces.json, and people.json rel paths accordingly. Album paths are
offered via a datalist autocomplete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
duplicate names
After moving a face to "New Person", the user is now taken directly to
that cluster's detail page. If it's a single unnamed cluster, the face
is shown prominently at the top. Typing an existing name on the name
form triggers a confirm dialog: OK merges into the existing person's
cluster, Cancel saves as a new separate person with the same name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Removed faces now go to an "Unidentified pool" cluster rather than
disappearing. Deleting a cluster blacklists all its members so they are
skipped by future re-clustering runs. Pool faces can be assigned to a
named person or individually blacklisted. A plain-English info box on
the detail page explains what each action does and that no photo files
are ever modified.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
On the cluster detail page: "Remove face" option in each face's move
dropdown removes it from the cluster entirely; "Delete cluster" button
(red, with confirmation) removes the whole cluster from people.json.
Moving the last face out of a cluster also auto-deletes it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Each cluster in /admin/people now links to a detail page showing all
faces in a grid. From there you can rename the cluster, move individual
faces to another named person (or spin off a new cluster), or merge the
entire cluster into another. Hovering any face crop shows the original
full photo for context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Server-side search index built from all album.json files + people.json,
cached in memory for 5 minutes. Each photo document includes filename,
album path words, title, caption, camera, date parts (year/month-name/
full date), and person names.
- Recursive-descent Boolean parser: AND (explicit or implicit between
consecutive terms), OR, NOT, with standard precedence.
- GET /search?q=... returns a photo grid (max 300 results) linking each
photo back to its album lightbox.
- Search box added to the site header; hidden on mobile.
- Results show filename, date, person names, and album path per photo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- 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 <noreply@anthropic.com>
|
|
- Strip all face code from update.rb; add shared log helper writing to
/opt/albumen/log/albumen.log with [update] prefix. update.rb now owns
only album.json; face_daemon.rb owns faces.json.
- New scripts/face_daemon.rb: polls MEDIA_ROOT for unprocessed images,
calls faces.py in batches, writes per-directory faces.json sidecars
atomically. Graceful SIGTERM/SIGINT shutdown between directories.
- New config/face_daemon.service: systemd unit running as albumen user,
Restart=on-failure, logs via SyslogIdentifier=albumen-faces.
- app.rb: add FACES_ENABLED constant; load_faces() helper reads faces.json;
album_files() merges face data into each entry as :faces field.
- Update README.md and DESIGN.md to document the new daemon architecture,
faces.json schema, and service management commands.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- process_dir() takes idx/total args and prints [N/total] prefix on every
Scanning/Skipping line so long runs are easy to monitor via tail -f
- faces_pending?() checks album.json for any image with faces: null when
faces.enabled is true; if found, the sentinel skip is bypassed so those
images get processed even though the directory mtime hasn't changed
- This handles the case where face detection is newly enabled on a library
that was previously indexed without it — no --force needed on subsequent
runs after the initial catch-up
Resume after abort: sentinel is only touched after atomic_write_json, so
an aborted directory reruns cleanly. Already-completed directories skip
normally; partially-detected batches rerun from scratch (safe/idempotent).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- faces.py: use model="cnn" (more accurate, better at angles/small faces/poor
lighting) instead of HOG; model comment explains the trade-off clearly
- faces.py: accept multiple image paths; process with ThreadPoolExecutor
(dlib releases GIL during C++ inference → genuine thread parallelism);
output JSON dict {path: [faces]} for batch calls
- update.rb: batch_detect_faces() collects all unprocessed images per
directory and calls faces.py once per directory rather than once per image,
avoiding repeated model load overhead
- update.rb: FACES_WORKERS read from config.yml faces.workers (default 4;
set to 20 in this install's config.yml on a 64-core Xeon)
- update.rb: process_dir() now takes idx/total and prints [N/total] prefix
on every Scanning/Skipping line for progress monitoring
To monitor a long run:
nohup ruby /opt/albumen/scripts/update.rb > /tmp/faces_update.log 2>&1 &
tail -f /tmp/faces_update.log
Resume/restart is fully safe: sentinel files are only written after
atomic_write_json, so an aborted directory reruns cleanly from scratch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- 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 <noreply@anthropic.com>
|
|
Hovering over a thumbnail in the admin file table pops up the full
300×300 cached version near the cursor, making it easy to confirm
identity before deleting or editing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- update.rb: skip exiftool on images marked exif_absent (set after first
failed attempt); prevents repeated slow scans of old photos with no EXIF
- update.rb: explicit directory argument now implies force — passing a path
always rescans that subtree regardless of sentinel mtime
- app.rb: /admin/update no longer hardcodes --force; sentinel-based skipping
is used by default, making UI updates finish in seconds instead of minutes
- admin/album.erb: add "Force rescan all" checkbox to Run Update button;
checked state passes force=1 to the server and restores --force behavior
- README.md, DESIGN.md: document sentinel skipping, exif_absent flag, and
explicit-directory force behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Lightbox: .lb-media now fills the full stage (flex, 100%x100%) and
#lb-img/#lb-video use width/height:100% + object-fit:contain so at
least one axis always reaches the edge. Click-to-close updated to
check event.target===stage instead of stopPropagation.
Slideshow: #ss-img/#ss-video likewise changed from max-width/max-height
to width/height:100% so small or portrait media fills the stage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
update.rb records transcoded_to in album.json (even on re-runs where
the MP4 already exists) so the marker survives across scans.
app.rb filters files with transcoded_to from non-admin views.
album.erb renders them greyed-out with an amber "⚠ original" badge in
admin mode. admin/album.erb marks the edit-table row and shows the
target filename under the original.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
On each run, any .avi, .mkv, or .mov file without a same-named .mp4
sibling is transcoded with ffmpeg (H.264/AAC, CRF 23, faststart).
The original is kept on disk and hidden in album.json so only the
playable MP4 appears in the UI; admins can un-hide the original if
needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Checking Delete and saving permanently removes the file and its
thumbnail; a JS confirm dialog gates the submit. Deleted files are
stripped from params before save_edits so they don't linger in
album.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Admin edit form: "Folder name" field renames the directory on save;
also moves the thumbnail cache subtree to match the new path
- Admin edit page: "Run Update" button spawns update.rb in a background
thread, streams output into a terminal-style log panel via 1.5s polling;
shows Done/Error status when complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Root album leaves @og_image_rel nil so layout.erb falls back to
/img/albumen.png; photo-based OG image only applies to sub-albums.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- public/img/albumen.png: app logo
- public/favicon.ico: 32x32 + 16x16 multi-size
- public/apple-touch-icon.png: 180x180 for iOS home screen
- layout.erb: favicon link tags; og:image falls back to albumen.png
when no specific photo is selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- slideshow_view now uses all_media_entries for non-root dirs so the
slideshow traverses sub-albums instead of only direct files
- album.erb condition simplified: show launcher when albums OR media
entries exist (previously hidden for folders with only sub-albums)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Groups media files in an album directory by EXIF date, reverse-geocodes
the first GPS fix of each day via Nominatim, and moves files into
"Day N - Location" subdirectories. Dry-run by default; pass --go to
execute. Supports a LOCATION_OVERRIDES map for cleaning up Nominatim
names that are overly granular.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Album cards show recursive photo count (bubbles up through sub-albums).
- Lightbox info panel shows camera, aperture, shutter speed, and ISO;
update.rb now extracts and stores these EXIF fields.
- Video thumbnail cards show a duration badge (e.g. "1:23").
- Slideshow launcher redesigned: button on its own line, with Shuffle /
Full screen / Interval options on a second line, all inside a rounded
border to make the grouping clear.
- Fixed album-actions alignment so Interval sits level with the checkboxes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When not shuffled, reverse SS_ENTRIES so the most recently added
photos play first instead of last.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
- Interval input (default 5 s) now lives beside Shuffle/Full screen on
the album page; passed as ?interval= param to the slideshow and seeded
into the hidden ss-interval input on load.
- Added "Interval" label text next to the input.
- Fixed slideshow controls being pushed off-screen on mobile by using
100dvh (dynamic viewport height) with 100vh as a fallback, so the
layout accounts for mobile browser chrome.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
When a media file is deleted from disk, also delete its cached
thumbnail from cache/thumbs/ so stale .th.jpg files don't accumulate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
The next timer was starting as soon as ssShow() was called, so preload
time + 500ms crossfade ate into the visible interval. Now the timer
starts only after applyEntry() fires (photo is actually on screen),
guaranteeing each photo gets a full interval of visibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Acquire a screen wake lock when the slideshow is playing; release on
pause or page hide; re-acquire when the tab returns to the foreground.
Failures are silently ignored so older browsers degrade gracefully.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Use Page Visibility API to cancel the timer when the tab hides and
reschedule fresh on return, so no queued ticks fire in a burst.
Also fix stale #photo= → ?photo= in stage click-through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|