v1.9.0 "Chatoyance"
Facet 1.9.0 "Chatoyance"
Burst detection groups on framing and capture time, which describes an exposure bracket exactly — so every bracket was collapsed into a burst and hidden behind whichever rung happened to score best, as likely the −2 EV frame as the correct one. On the 126k-photo library this was measured against, that was 226 sets, every one of them swallowed, and nobody had noticed because the symptom was photos quietly missing rather than anything failing. Facet now reads the exposure ladder out of EXIF it already stores, so an existing library is labelled by arithmetic alone. Alongside it: the details panel stops chasing the cursor, any selection can be compared side by side, and the viewer tells you when a newer Facet exists.
Highlights
- Exposure brackets are recognised as brackets (
--detect-sequences,sequence_detectionconfig,sequence_group_id/sequence_kind/sequence_ev_offset). The ladder is derived from thef_stop/shutter_speed/ISOalready stored per photo — no rescan, no image decode, no model — and a burst that turns out to be exactly one bracket has its lead moved onto the base exposure. Frames carry their compensation signed the way a camera labels an AEB set,-2dark and+2bright. Panorama detection is deliberately absent: its frames share one exposure and differ by a pan, which wants its own evidence rather than a guess bolted onto this. - Culling can be browsed by exposure bracket (
GET /api/culling-groups?group_by=bracket). Kept out of the mergedallfeed on purpose — the point is to see a bracket apart from the bursts it would otherwise be read as. Every frame starts marked keep, and confirming records no comparison pairs: preferring one rung of an exposure ladder describes the exposure, not the photograph, and feeding that to the ranker would bias every later ranking. - A "Best of bracket" gallery filter, on by default (
hide_brackets). Deliberately independent of "Best of Burst" rather than relying on it — a quarter of real bracket sets share a burst group with unrelated frames, where the burst lead is not the base exposure and the flanking frames stayed on show. Measured on a 126k library: 549 frames hidden with burst-hiding off, and counted in the hidden-photos banner so "Show all" still reveals every one. - Auto-cull can trim brackets that earned nothing (
trim_brackets, off by default). A bracket buys latitude the base exposure clips away; when that base clips neither shadows nor highlights there was nothing to recover. Sets whose base has never been measured are left alone — unmeasured is not unclipped. - A docked details panel (
tooltip_mode: "panel"). The floating tooltip moves with the cursor, which makes comparing the same field across photos guesswork. The panel shares the end drawer with the filter sidebar — one strip, so the grid keeps exactly the width it has whenever the filters are open. It follows both hover and click, since a parked panel has no reason to insist on one gesture and clicking is the only path on a touch screen, and Space reaches it from the keyboard. - Compare any selection of photos side by side. The synced pan/zoom compare already existed but could only reach frames sitting next to each other inside a burst, so deciding between two shots taken minutes apart had no surface at all. Reuses the culling lightbox's pane wholesale, full-resolution swap past the fit scale included.
- Invert the gallery selection, bounded to the loaded photos like Select all: picking the keepers and inverting is the direct way to see what is about to be rejected.
- The culling feed sorts oldest-first and reverses.
sort=chronologicalfollows the shoot forward and is the one mode that also orders photos within a group by capture time, which is the only way a bracket or a panned run reads correctly; an arrow button flips whichever mode is active. - A weekly notice when a newer Facet has been released (
GET /api/updates/check,updatesconfig block). Distinct from the browser's existing "a new version is available", which swaps the page onto a bundle it has already downloaded. The check runs server-side and its result is cached, so an install asks GitHub at most once perinterval_dayshowever many people have the viewer open, and the request carries nothing but itself — no token, no identifiers, nothing about the library. A failure is silent.
Fixed
- A burst that stops being a bracket gets its scored lead back. Re-running
--detect-sequencescleared the sequence columns of every frame it no longer recognised — corrected EXIF, a tightened threshold — but left "best of burst" pointing at the base exposure the earlier pass had promoted, and since that pass is the only writer of the flag outside a full rescan, nothing would have corrected it. A burst that merely contains a bracket is untouched, however often the pass re-runs. - Smart album covers ignored the bracket default, so an album could be represented by a flanking dark or bright exposure while the album's own gallery view hid exactly that frame.
- The release check no longer blocks the event loop. It issued a synchronous outbound request from an
asyncroute and held the loop for up to its timeout, stalling every other request the viewer was serving. - "Show all" on the hidden-photos banner is reversible. It cleared the blink, burst and duplicate filters one way, so glancing at what a filter was holding back meant walking to the sidebar and re-ticking three boxes from memory.
- The mobile bulk-actions sheet was missing Export to editor and Cull to folder. Both sat on the desktop bar only, and that sheet is the sole route to any bulk action on a phone.
- Selecting the docked details panel on a narrow viewport no longer strands a tooltip mid-page. The mode skipped the placement maths on the assumption the rail was showing; it now falls back to a positioned floating tooltip, and the menu stops offering a mode the viewport cannot render.
- The header navigation no longer shows a scrollbar when everything fits. Each Material icon button hangs its 48px touch target 6px past its 36px box, and the scrolling container counted that overhang as content: at 900px the icons occupied exactly the space available and the bar still scrolled.
- The bracket culling feed filtered on an unindexed column.
EXPLAIN QUERY PLANshowed a full scan ofphotoson every load; an index on(sequence_kind, sequence_group_id)turns it into a seek.
Security
- The bracket confirm scopes its response counts to what the caller can see. The write was never at risk — rejection resolves the visible subset itself — but the kept/rejected/skipped counts were derived before that filter, so under multi-user directory scoping they answered "does this path exist, and is it a bracket frame?" for directories the caller cannot read.
- The release check is bounded in size and in shape. Its timeout bounds each socket operation rather than the whole transfer, so an endpoint answering correctly and then trickling bytes grew the buffer without a limit; and the tag was read off the payload outside the error handling, so a proxy replying with valid JSON that is not an object left as an unhandled 500 — the one thing the check documents it must never do.
Upgrade notes
--detect-sequencesruns automatically at the end of every scan and as a step of--upgrade-db, so you only need to run it by hand if you want the bracket labels immediately. Roughly a minute on 126k photos.- "Best of bracket" is on by default, so a bracket contributes only its base exposure to the grid. The hidden frames are counted in the hidden-photos banner, and
viewer.defaults.hide_brackets: falserestores the old behaviour. idx_sequence_kindis created on the nextpython database.py(or any startup that initialises the schema). Until then the bracket culling feed still works, just without the index.trim_bracketsis a flag onPOST /api/culling/auto, not a config key and not yet a checkbox in the auto-cull dialog. Dry-run remains the default.- The details panel needs a viewport of at least 1280px. Below that the option is not offered at all, rather than offered and silently ignored.
- The update check is opt-out:
updates.enabled: falsestops the outbound request entirely. It is edition-only, and each browser shows the notice at most once a week.
Full changelog: https://github.com/ncoevoet/facet/blob/master/CHANGELOG.md