Merge: group the frames of one moment, and let a burst fold away
FR-CULL-5. Frames join a burst when they are adjacent in time and look like the frame before them -- both, because time alone groups a whole ceremony and similarity alone groups a studio setup across two days. Adjacent pairs only, chained; there is no all-pairs step and there must never be one. No selection of any kind. The representative is the earliest frame, a fact about the clock rather than a judgement about the photograph, and a newly found burst arrives open, so the pass never takes a row off the screen. Verified: fmt, clippy --workspace --all-targets -D warnings, 346 dr-catalog tests, 511 dr-ui tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
||||
//! - [`collections`] — the collection tree and membership the UI edits
|
||||
//! - [`keywords`] — the keyword vocabulary and what it is assigned to
|
||||
//! - [`faces`] — detected faces, the people they belong to, and who said so
|
||||
//! - [`bursts`] — frames that are one moment, grouped so they judge as one
|
||||
//! - [`jobs`] — the durable background work queue
|
||||
//! - [`trash`] — soft delete to a folder, then permanent delete
|
||||
//! - [`merge`] / [`sync`] — cross-device merging of collections and keywords
|
||||
@@ -33,6 +34,7 @@ use std::path::Path;
|
||||
use dr_types::{Availability, ImageId};
|
||||
use rusqlite::Connection;
|
||||
|
||||
pub mod bursts;
|
||||
pub mod cache;
|
||||
pub mod collections;
|
||||
pub mod dedup;
|
||||
|
||||
@@ -15,7 +15,7 @@ use rusqlite::Connection;
|
||||
use crate::error::CatalogError;
|
||||
|
||||
/// Schema version this build writes and understands.
|
||||
pub const SCHEMA_VERSION: i64 = 10;
|
||||
pub const SCHEMA_VERSION: i64 = 11;
|
||||
|
||||
/// Apply migrations up to [`SCHEMA_VERSION`].
|
||||
///
|
||||
@@ -98,6 +98,13 @@ pub fn migrate(conn: &Connection) -> Result<i64, CatalogError> {
|
||||
tx.commit()?;
|
||||
}
|
||||
|
||||
if from < 11 {
|
||||
let tx = conn.unchecked_transaction()?;
|
||||
tx.execute_batch(V11)?;
|
||||
tx.pragma_update(None, "user_version", 11)?;
|
||||
tx.commit()?;
|
||||
}
|
||||
|
||||
Ok(from)
|
||||
}
|
||||
|
||||
@@ -205,6 +212,11 @@ pub fn v1_for_attached(schema_name: &str) -> String {
|
||||
/// creating it over there would fail on columns that are not there. Nothing is
|
||||
/// lost by its absence — it exists to make the *grid* page quickly, and the
|
||||
/// grid never reads across an attachment.
|
||||
///
|
||||
/// V11 is excluded on the same grounds and for the plainer reason that a merge
|
||||
/// has nothing to do with it: burst grouping is rebuilt locally from local
|
||||
/// signatures, and no code reads a remote catalog's `burst_*` tables. Its
|
||||
/// `ALTER TABLE` would fail here anyway, being unqualifiable by the rewrite.
|
||||
pub fn for_attached(schema_name: &str) -> String {
|
||||
// V10 is `ALTER TABLE`, which the textual rewrite cannot qualify, so its
|
||||
// columns are spelled out. A remote genuinely older than V10 is a real
|
||||
@@ -397,6 +409,73 @@ ALTER TABLE people ADD COLUMN ignored INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE faces ADD COLUMN crop BLOB;
|
||||
"#;
|
||||
|
||||
/// TRACES: FR-CULL-5
|
||||
/// Burst grouping: which frames are one moment, and which one stands for it.
|
||||
///
|
||||
/// The reasoning behind the grouping itself is in [`crate::bursts`]; what
|
||||
/// belongs here is why it is stored in three pieces rather than one.
|
||||
///
|
||||
/// **`images.perceptual_hash` is a column, not a table**, for the same reason
|
||||
/// `content_hash` is: it is one number per image, NULL until something has had
|
||||
/// the pixels in hand, and every query that wants it is already reading the
|
||||
/// image row. It is local derived state — a rebuilt catalog recomputes it from
|
||||
/// thumbnails — which is also why it is absent from [`for_attached`], alongside
|
||||
/// the shadowing and trashing columns V2 through V5 add.
|
||||
///
|
||||
/// **`burst_members` is rewritten whole by every pass.** No id of its own: the
|
||||
/// group is named by the image id of its earliest frame, so a burst that has not
|
||||
/// changed keeps its name across a regroup and the interface can remember that
|
||||
/// this one is open. There is no `bursts` table to go with it because a group
|
||||
/// has no properties beyond its members — inventing a row for it would create an
|
||||
/// identity that survives the grouping being rebuilt, which is precisely what
|
||||
/// must not happen.
|
||||
///
|
||||
/// **`burst_pick` is the one thing here that is not derived**, and it is a
|
||||
/// separate table so that rewriting the grouping cannot erase it. A
|
||||
/// representative stored on `burst_members` would be forgotten every time a
|
||||
/// frame arrived; the user would be asked the same question after every scan.
|
||||
/// The same argument `people.ignored` makes in V10, one subsystem over.
|
||||
///
|
||||
/// **`burst_expanded` is view state in the catalog**, which is unusual enough to
|
||||
/// justify. The grid is a window over an ordered query — `LIMIT n OFFSET k` —
|
||||
/// so what a collapsed burst hides has to be decided by the query, or the row
|
||||
/// count stops agreeing with the scrollbar and the ordinals a scrub resolves to.
|
||||
/// Once SQL has to see it, this is where it lives. Nothing else reads it, and it
|
||||
/// is emptied of stale groups by every pass.
|
||||
const V11: &str = r#"
|
||||
-- A 64-bit perceptual signature. Local derived state: NULL until something has
|
||||
-- decoded the image, recomputed from thumbnails if the catalog is rebuilt, and
|
||||
-- comparable only to signatures produced by the same build (`bursts`).
|
||||
ALTER TABLE images ADD COLUMN perceptual_hash INTEGER;
|
||||
|
||||
CREATE TABLE burst_members (
|
||||
-- One burst at most per image: a frame belongs to the moment it was taken
|
||||
-- in, and nothing else.
|
||||
image_id INTEGER PRIMARY KEY REFERENCES images(id) ON DELETE CASCADE,
|
||||
-- The image id of the burst's earliest frame. Not a foreign key by
|
||||
-- accident: the leader is itself a member, so this genuinely references
|
||||
-- images(id), and cascading its deletion is right.
|
||||
burst_id INTEGER NOT NULL REFERENCES images(id) ON DELETE CASCADE,
|
||||
-- The frame the group collapses to. Exactly one per burst.
|
||||
representative INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
-- Counting a burst's frames and listing them are what the grid asks for, once
|
||||
-- per window; without this both are a scan of every grouped frame in the
|
||||
-- library.
|
||||
CREATE INDEX burst_members_burst ON burst_members(burst_id);
|
||||
|
||||
-- The user's own choice of representative. User data, never rewritten by a
|
||||
-- grouping pass -- see the module doc above.
|
||||
CREATE TABLE burst_pick (
|
||||
image_id INTEGER PRIMARY KEY REFERENCES images(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Bursts the grid is currently showing in full.
|
||||
CREATE TABLE burst_expanded (
|
||||
burst_id INTEGER PRIMARY KEY REFERENCES images(id) ON DELETE CASCADE
|
||||
);
|
||||
"#;
|
||||
|
||||
const V9: &str = r#"
|
||||
-- TRACES: FR-CULL-8
|
||||
-- A record that face detection has *run* on an image, distinct from what it
|
||||
|
||||
Reference in New Issue
Block a user