Highlights: still photos are first-class (RAW/JPEG/HEIC, EXIF, one corpus with
video); bursts and RAW+JPEG pairs share one assessment; ratings reach Lightroom
via .xmp sidecars; fdx-mcp serves the archive to any MCP host (Claude
Desktop, LM Studio with a local model) with a contact-sheet tool and a
user_rating write; the resumability promise now holds under Ctrl-C, re-runs,
and hostile input. Breaking: the install is extras-scoped (.[video],
.[images], .[photos], .[mcp], .[all]); pip install -e . alone no
longer pulls the video stack.
Added
-
fdx-mcp: the archive as MCP tools. A new optional command ([mcp]
extra: the official MCP SDK + Pillow) serves indexed roots to any MCP host
over stdio (Claude Code, Claude Desktop, LM Studio with a local model).
Tools:list_roots,query_media(a subset offdx-query's metadata filters plusfolder
andoffset/limitpaging, effective ratings, the scene sentence),
read_sidecar,archive_overview(_INDEX.md, labelled as a snapshot),
contact_sheet(1-20 files rendered into one numbered 4-column grid with a
legend; a clip contributes one frame at itsnotable_timestampor
midpoint), andset_user_rating. Every path, including paths derived from
sidecars, is resolved and must lie under a configured root;--read-only
removes the setter. fdx-mcp makes no model call; each successful
contact-sheet call is one bounded image, and host inference is where cost
and data flow live. Seedocs/mcp.md. -
user_rating: the person's decision next to the model's.
set_user_ratingwritesuser_rating(keep/review/cull),user_note, and
user_rated_atinto the sidecar frontmatter atomically, body preserved
byte-for-byte; the model'sratingis never modified. A valid user rating
wins everywhere ratings are read:fdx-query --ratingand
--with-description(keep (user)), JSON records gaineffective_rating,
fdx-master(counts, a "User ratings" line,(user)in the cull list),
fdx-xmp(stars, plus the keyworduser-rated). Re-indexing carries the
keys over. An invalid value is reported once and ignored. -
fdx-query --folder SUBand--offset N(paging with--limit;
--countnow reports matches before paging). The CLI and the MCP tool
share oneFilters/run_queryimplementation. -
Burst grouping + RAW/JPEG pairing: one assessment per group. Before
the per-file loop,fdxgroups a folder's stills: a RAW and its same-stem
camera JPEG become one pair (RAW primary, JPEG as preview source), and at
least 3 frames in one folder with matching camera make/model and gaps of at
most 2s become a burst (a burst of pairs is one burst). Each group gets a
single vision call on its primary (the member with the highest local
Laplacian sharpness score; no model choice); every alternate gets a stub
sidecar that copies the primary's assessment fields (not faces), carries its
own EXIF/GPS, says so in its body, and points at the primary via agroup:
block. Vision calls per run = groups + ungrouped files, never more than
before. Resume is group-aware: stubs are written before the primary, the
primary's old sidecar is removed first, and an incomplete or changed group is
reprocessed together (repeating that group's call).--no-group(bothfdx
andfdx-photos; a documented no-op in the latter until Photos-native burst
support) disables grouping for a run;--force --no-groupre-indexes a
grouped folder individually. A failed batched EXIF read stops the run with a
clear message instead of silently indexing every file.fdx-mastercounts
ratings, keywords, faces, and the cull list over primaries only and reports
Grouped: N files in M groups;fdx-query --primary-onlyhides stubs;
fdx-xmptags burst membersburst-primary/burst-alternate. Thresholds
are constants documented indocs/tuning.md. No new dependency. -
fdx-xmp— get framedex ratings into Lightroom. A new standalone command
that projects the rating, keywords, and one-line scene caption from
.description.mdsidecars into standard.xmpsidecars next to proprietary-RAW
originals, so Lightroom Classic / Bridge pick them up via Read Metadata from
Files.keep/review/cull→ 3★/2★/1★,cullalso gets a Red label;
keywords + scene_type →dc:subject; the Scene sentence →dc:description.
It's a regenerable view (delete every.xmpand re-run) and strictly
non-destructive: it never edits an original and never overwrites a.xmpit
didn't write — a foreign or hand-edited sidecar is reported as a conflict and
skipped (ownership tracked by a content hash in_XMP_MANIFEST.json).
--dry-runpreviews. Pure stdlib, no new dependency. Scope in v1 is
proprietary RAW → Lightroom (.dngand JPEG/HEIC/TIFF embed metadata in-file,
which framedex never touches). -
Content-diverse frame sampling (issue #5). The 5 vision frames are now the
most mutually different, sharpest moments of a clip (H-S histogram
farthest-point selection over a fast-seek thumbnail pool) instead of
evenly-spaced grabs, so a clip that pans across scenes no longer sends five
near-duplicates. Exactly 5 frames as before — vision cost is unchanged — and
static/short clips keep the old even spacing.--frame-sampling even
restores legacy behavior. No new dependencies. -
Still-photo indexing.
fdxnow indexes photos (RAW / JPEG / HEIC) alongside
video in a single pass.--media images|videos|allscopes a run. Photos get the
same.description.mdsidecars — an EXIFcamera:block (make, lens, focal
length, aperture, shutter, ISO), GPS + reverse-geocoded place, face detection,
and an AI scene description with keywords and a keep/review/cull rating — and are
queryable withfdx-queryand rolled up byfdx-master. RAW is read from the
embedded JPEG preview (no libraw); EXIF orientation is normalized. -
fdx-query --media image|videofilter (accepts the pluralimages/videos
too);fdx-masterreports media-neutral counts.
Fixed
- exiftool failures are errors, not blanks.
get_image_metadataand
get_gpsnow raise when exiftool exits non-zero or returns no JSON, instead
of silently producing a sidecar with an empty camera block / no location. A
per-file run reports the file as an error and retries it next time; a
burst/pair group fails before its vision call. - Atomic writes use exclusive, unique temp files.
atomic_write_text
(every sidecar, index, and XMP write) now creates its same-directory temp via
mkstemp: two writers can't collide on one temp name, and a symlink planted
at a predictable temp name can no longer redirect a write into another file.
Stale temps are.<name>.<random>.tmp(still hidden, still safe to delete). - Sidecar parsers are fence-aware.
fdx-query,fdx-master,fdx-xmp,
fdx-summary, and the resume check now end the frontmatter at a line that
is exactly---, so a filename or value containing---no longer
truncates the YAML. - Trust hardening — the resumability/idempotency promise now holds under
Ctrl-C, re-runs, and hostile input.- Sidecars and the
_INDEX.*/ folder-summary files are written atomically
(temp file +os.replace), so an interrupt or disk-full mid-write can no
longer leave a truncated, permanently-"indexed" file. - Faces are committed to
faces.dbbefore the sidecar (the sidecar is the
resume marker), and face rows are now rewritten even when a re-run detects
zero faces — so a crash between the two writes, or a changed detection
result, no longer strands stale rows. faces.dbis now idempotent: re-runs dedupe onvideo_pathor
sidecar_path(fixing duplicate rows forfdx-photos --downloadassets,
whose temp path changes each run),member_countis recomputed instead of
incremented (no more inflation on--force), and unnamed zero-member
clusters are reaped while user-named ones are kept.fdx-photos --since/--untilno longer crashes with a timezone
TypeErrorwhen the library has tz-aware dates or any undated asset.- The
clivision backend no longer runsclaude -pwith
bypassPermissions. It uses--permission-mode dontAskplus a read-only
Readallowlist (Bash/writes denied), so untrusted transcript /
.video-context.mdtext embedded in the prompt can't drive tool use. - A vision response with no parseable YAML block is retried on the next run
(loud stderr line) instead of being silently written as a defaults-only
sidecar (rating: review, allunclear) that skipped the file forever. fdx-queryandfdx-masternow warn and skip a sidecar whosepathfield
is missing, blank, or non-string instead of crashing or emitting the
.description.mdpath as if it were the media path; Photos-managed assets
(which omitpathby design) are unaffected. Closes #14.
- Sidecars and the
- Frame timestamps recorded for face detection (
faces.dbframe_time) now
come from the actual extraction instead of being re-derived, fixing a silent
desync when a frame write failed mid-clip. - Vision-backend failures (timeout, HTTP error, permission-denied) no longer write
a junk sidecar that would permanently skip the file — the item is reported as an
error and retried on the next run. fdx-queryduration filters (--min/max-duration) no longer match photos, which
have no duration.
Changed
- Install is now extras-scoped so a photo-only setup never pulls torch:
framedex[images]— Pillow + pillow-heif (still photos)framedex[video]— whisperx/torch (video indexing)framedex[photos]— osxphotos + the video stack (Apple Photos)framedex[all]— everything- Breaking:
pip install -e .no longer installs the video stack. Existing
video workflows need'.[video]'(or'.[all]'). A video run without it now
prints a clear "install the video extra" message instead of a stack trace.
- Internal: media-agnostic logic extracted into
framedex/pipeline.py;
whisperx/torchare imported lazily. Video sidecar output is byte-identical. - Docs: README split into focused guides. The README now covers the core
workflow; deeper and edge-case topics moved todocs/and are linked near the top:
docs/apple-photos.md(the Apple Photos library + iCloud "Optimize Storage" edge
case),docs/tuning.md(folder context, proper-noun biasing, languages,
diarization setup), anddocs/troubleshooting.md. An ASCII flow diagram at
the top of the README shows the pipeline at a glance. No behavior change.