<feed xmlns='http://www.w3.org/2005/Atom'>
<title>albumen.git/scripts/update.rb, branch main</title>
<subtitle>Ruby/Sinatra photo album</subtitle>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/'/>
<entry>
<title>Separate face detection into standalone daemon</title>
<updated>2026-06-08T18:36:07+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T18:36:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=625b3d5176f2c274e91fcf28bda8e45cc0477722'/>
<id>625b3d5176f2c274e91fcf28bda8e45cc0477722</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add progress counter and faces-pending sentinel bypass to update.rb</title>
<updated>2026-06-08T17:37:06+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T17:37:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=ecc872a1fd43c0863e3171a1faf533adc3e3a4c5'/>
<id>ecc872a1fd43c0863e3171a1faf533adc3e3a4c5</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Switch face detection to CNN model with parallel batch processing</title>
<updated>2026-06-08T17:34:18+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T17:34:18+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=73d6f8c9ac0177ca3a6587e6534592a545d44d67'/>
<id>73d6f8c9ac0177ca3a6587e6534592a545d44d67</id>
<content type='text'>
- 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 &gt; /tmp/faces_update.log 2&gt;&amp;1 &amp;
  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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &gt; /tmp/faces_update.log 2&gt;&amp;1 &amp;
  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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add opt-in facial recognition: detection and embedding storage</title>
<updated>2026-06-08T17:09:51+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-06-08T17:09:51+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=da28a20f091372375822f9dde4486ecade859e7e'/>
<id>da28a20f091372375822f9dde4486ecade859e7e</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Speed up update.rb and fix UI always forcing full rescan</title>
<updated>2026-05-22T22:50:35+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-05-22T22:50:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=d32b5e99afc6f0cffefa594510cda0e4f414db75'/>
<id>d32b5e99afc6f0cffefa594510cda0e4f414db75</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Hide transcoded originals from non-admins; mark them visually for admins</title>
<updated>2026-05-14T22:59:59+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-05-14T22:59:59+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=c76ea393777897e0c367e186d1a3b243193d8377'/>
<id>c76ea393777897e0c367e186d1a3b243193d8377</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Auto-transcode non-browser-playable videos to MP4 in update.rb</title>
<updated>2026-05-14T22:53:09+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-05-14T22:53:09+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=6acd47c1ca27d705afe88b292a55a5170c038d2e'/>
<id>6acd47c1ca27d705afe88b292a55a5170c038d2e</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Add photo counts, EXIF details, video duration badges, slideshow launcher UI</title>
<updated>2026-05-12T14:45:00+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-05-12T14:45:00+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=b47fdda4fe1bf6fe90d0ba30eedac435dde7c034'/>
<id>b47fdda4fe1bf6fe90d0ba30eedac435dde7c034</id>
<content type='text'>
- 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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
- 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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Prune orphaned thumbnails when update.rb removes deleted files</title>
<updated>2026-05-12T12:17:40+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-05-12T12:17:40+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=3e1a71be6e63696dfe9792f123f24ece1da8116a'/>
<id>3e1a71be6e63696dfe9792f123f24ece1da8116a</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
<entry>
<title>Fix taken_at timezone: store and display as camera local time</title>
<updated>2026-05-09T15:53:18+00:00</updated>
<author>
<name>Ken D'Ambrosio</name>
<email>ken@jots.org</email>
</author>
<published>2026-05-09T15:53:18+00:00</published>
<link rel='alternate' type='text/html' href='https://git.jots.org/albumen.git/commit/?id=fa36e54d878a3274f7728eb0b84c351b33f3c6ed'/>
<id>fa36e54d878a3274f7728eb0b84c351b33f3c6ed</id>
<content type='text'>
EXIF DateTimeOriginal has no timezone — it's the camera's wall clock.
Storing it via .iso8601 attached +00:00 (server TZ), causing browsers
to shift the time to their local zone when parsing. Switch to
strftime('%Y-%m-%dT%H:%M:%S') so no offset is written. JS strips any
existing +00:00 suffix from already-stored values so old data is also
displayed correctly.

Co-Authored-By: Claude Sonnet 4.6 &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
EXIF DateTimeOriginal has no timezone — it's the camera's wall clock.
Storing it via .iso8601 attached +00:00 (server TZ), causing browsers
to shift the time to their local zone when parsing. Switch to
strftime('%Y-%m-%dT%H:%M:%S') so no offset is written. JS strips any
existing +00:00 suffix from already-stored values so old data is also
displayed correctly.

Co-Authored-By: Claude Sonnet 4.6 &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
</feed>
