Merge: face quality gates, and identity filters that combine
Build and test / Desktop (Linux) (push) Successful in 32m56s
Build and test / Layer separation (push) Successful in 50s
Traceability / Requirement traces (push) Successful in 39s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 52m43s
Build and test / Desktop (Linux) (push) Successful in 32m56s
Build and test / Layer separation (push) Successful in 50s
Traceability / Requirement traces (push) Successful in 39s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 52m43s
Two things the People screen was missing. Faces were being indexed at any size and any sharpness — 40 pixels on the box and no blur gate at all — so most of what the library held was background strangers and motion-blurred passers-by, and the blurred ones were quietly bridging unrelated clusters. Both floors are now measured on the real library with `face_index --quality` rather than guessed: 64 source pixels across the aligned crop, and a contrast-invariant sharpness of 0.020. And the grid could only ever be narrowed to one person, which cannot express "the pictures the two of them are in together". The filter now holds a set with a union/intersection mode, built a person at a time from the Identity screen and taken apart chip by chip on the filter bar. fmt, clippy -D warnings and the full workspace suite pass on the merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-9
@@ -6,7 +6,7 @@ import { CropOverlay } from "crop.slint";
|
||||
import { HistoryPanel, HistoryRow } from "history.slint";
|
||||
import { LaunchScreen } from "launch.slint";
|
||||
import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow } from "library.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip } from "library.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
||||
import { CollectionsPanel, CollectionRow, OfflinePrompt } from "collections.slint";
|
||||
import { HistogramPanel, HistogramView } from "histogram.slint";
|
||||
@@ -14,7 +14,7 @@ import { SettingsPage } from "settings.slint";
|
||||
import { ImportPage } from "import.slint";
|
||||
import { StatusBar, InfoPanel } from "develop.slint";
|
||||
|
||||
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView }
|
||||
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView, PersonChip }
|
||||
export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole }
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
@@ -366,7 +366,7 @@ export component AppWindow inherits Window {
|
||||
in property <bool> identity-selected-ignored: false;
|
||||
callback identity-toggle-show-ignored();
|
||||
callback identity-ignore-person(int, bool);
|
||||
callback identity-show-photos(int);
|
||||
callback identity-show-photos(int, bool);
|
||||
callback identity-index();
|
||||
callback identity-stop-indexing();
|
||||
callback identity-check-coverage();
|
||||
@@ -542,9 +542,12 @@ export component AppWindow inherits Window {
|
||||
/// the two arguments is -1, saying which axis was *not* meant.
|
||||
callback library-judged(int, int);
|
||||
|
||||
/// Whose photographs the grid is narrowed to. Empty when it is not.
|
||||
in property <string> library-filter-person-name;
|
||||
callback library-filter-person-cleared();
|
||||
/// Whose photographs the grid is narrowed to, one entry each.
|
||||
in property <[PersonChip]> library-filter-people;
|
||||
/// Whether those people are an intersection rather than a union.
|
||||
in property <bool> library-filter-people-all: false;
|
||||
callback library-filter-person-cleared(int);
|
||||
callback library-filter-people-mode-toggled();
|
||||
in property <int> library-filter-min-rating: 0;
|
||||
in property <bool> library-filter-unjudged: false;
|
||||
in property <int> library-filter-flag: 0;
|
||||
@@ -1199,6 +1202,7 @@ in property <bool> panel-visible: true;
|
||||
confirm-all() => { root.identity-confirm-all(); }
|
||||
split-picked() => { root.identity-split-picked(); }
|
||||
compact: !root.expanded;
|
||||
photos-filtered: root.library-filter-people.length > 0;
|
||||
regrouping: root.identity-regrouping;
|
||||
regroup-status: root.identity-regroup-status;
|
||||
ignored-count: root.identity-ignored-count;
|
||||
@@ -1206,7 +1210,7 @@ in property <bool> panel-visible: true;
|
||||
selected-ignored: root.identity-selected-ignored;
|
||||
toggle-show-ignored() => { root.identity-toggle-show-ignored(); }
|
||||
ignore-person(id, on) => { root.identity-ignore-person(id, on); }
|
||||
show-photos(id) => { root.identity-show-photos(id); }
|
||||
show-photos(id, add) => { root.identity-show-photos(id, add); }
|
||||
recluster() => { root.identity-recluster(); }
|
||||
index-faces() => { root.identity-index(); }
|
||||
stop-indexing() => { root.identity-stop-indexing(); }
|
||||
@@ -1432,7 +1436,8 @@ in property <bool> panel-visible: true;
|
||||
root.library-remove-from-collection();
|
||||
}
|
||||
|
||||
filter-person-name: root.library-filter-person-name;
|
||||
filter-people: root.library-filter-people;
|
||||
filter-people-all: root.library-filter-people-all;
|
||||
filter-min-rating: root.library-filter-min-rating;
|
||||
filter-unjudged: root.library-filter-unjudged;
|
||||
filter-flag: root.library-filter-flag;
|
||||
@@ -1463,7 +1468,8 @@ in property <bool> panel-visible: true;
|
||||
root.library-filter-unjudged-changed(on);
|
||||
}
|
||||
filter-flag-changed(f) => { root.library-filter-flag-changed(f); }
|
||||
filter-person-cleared() => { root.library-filter-person-cleared(); }
|
||||
filter-person-cleared(id) => { root.library-filter-person-cleared(id); }
|
||||
filter-people-mode-toggled() => { root.library-filter-people-mode-toggled(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +241,14 @@ export component IdentityScreen inherits Rectangle {
|
||||
/// Set a person aside, or bring them back.
|
||||
callback ignore-person(int, bool);
|
||||
/// Show every photograph this person appears in, in the library grid.
|
||||
callback show-photos(int);
|
||||
///
|
||||
/// The flag is whether to *add* to whoever the grid is already narrowed
|
||||
/// to, rather than replace them — which is how a union or an intersection
|
||||
/// of several people gets built without a person-picker of its own.
|
||||
callback show-photos(int, bool);
|
||||
/// Whether the library grid is already narrowed to somebody, so the
|
||||
/// "and also" button only appears when there is something to add to.
|
||||
in property <bool> photos-filtered: false;
|
||||
callback recluster();
|
||||
callback index-faces();
|
||||
callback stop-indexing();
|
||||
@@ -479,7 +486,14 @@ export component IdentityScreen inherits Rectangle {
|
||||
// screen is a filing cabinet with no drawer handles.
|
||||
if root.selected-person >= 0: Button {
|
||||
text: "Show photos";
|
||||
clicked => { root.show-photos(root.selected-person); }
|
||||
clicked => { root.show-photos(root.selected-person, false); }
|
||||
}
|
||||
// Builds the union or intersection a person at a time. Only
|
||||
// offered once the grid is narrowed to somebody, because
|
||||
// "and also" with nothing to add to is just "Show photos".
|
||||
if root.selected-person >= 0 && root.photos-filtered: Button {
|
||||
text: "And also…";
|
||||
clicked => { root.show-photos(root.selected-person, true); }
|
||||
}
|
||||
// Most clusters in a real library are strangers — passers-by,
|
||||
// other people's guests, a face on a poster. Naming them is
|
||||
|
||||
@@ -414,6 +414,12 @@ export component Timeline inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
/// One person the grid is narrowed to, as drawn on the filter bar.
|
||||
export struct PersonChip {
|
||||
id: int,
|
||||
name: string,
|
||||
}
|
||||
|
||||
export struct LibraryCell {
|
||||
name: string,
|
||||
// Non-empty on the first cell of a new month, e.g. "August 2026". The grid
|
||||
@@ -1253,10 +1259,13 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// grid reads as broken.
|
||||
in property <[int]> rating-counts;
|
||||
|
||||
/// Whose photographs the grid is narrowed to, for the chip that says so
|
||||
/// and clears it. Empty when no person filter is applied.
|
||||
in property <string> filter-person-name;
|
||||
callback filter-person-cleared();
|
||||
/// Whose photographs the grid is narrowed to — one chip each, so a
|
||||
/// selection of three can be taken apart one person at a time.
|
||||
in property <[PersonChip]> filter-people;
|
||||
/// Whether those people are an intersection rather than a union.
|
||||
in property <bool> filter-people-all: false;
|
||||
callback filter-person-cleared(int);
|
||||
callback filter-people-mode-toggled();
|
||||
callback filter-min-rating-changed(int);
|
||||
callback filter-unjudged-toggled(bool);
|
||||
callback filter-flag-changed(int);
|
||||
@@ -1817,7 +1826,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// Hidden while there is nothing to filter: an empty library offering
|
||||
// six rating buttons is chrome describing data that does not exist.
|
||||
if root.total > 0 || root.filter-min-rating > 0 || root.filter-unjudged
|
||||
|| root.filter-flag > 0 || root.filter-person-name != "": Rectangle {
|
||||
|| root.filter-flag > 0 || root.filter-people.length > 0: Rectangle {
|
||||
height: 34px;
|
||||
background: Theme.surface;
|
||||
|
||||
@@ -1859,12 +1868,22 @@ export component LibraryGrid inherits Rectangle {
|
||||
// that has to be the first thing read — a user who does not
|
||||
// find it is looking at a library that has apparently lost
|
||||
// most of its photographs.
|
||||
if root.filter-person-name != "": FilterChip {
|
||||
for p[i] in root.filter-people: FilterChip {
|
||||
icon: "cross";
|
||||
label: root.filter-person-name;
|
||||
label: p.name;
|
||||
active: true;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.filter-person-cleared(); }
|
||||
clicked => { root.filter-person-cleared(p.id); }
|
||||
}
|
||||
|
||||
// Only with two, because with one the modes are the same
|
||||
// filter and a toggle that changes nothing is a control that
|
||||
// teaches the user it does nothing.
|
||||
if root.filter-people.length > 1: FilterChip {
|
||||
label: root.filter-people-all ? "all of them" : "any of them";
|
||||
active: root.filter-people-all;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.filter-people-mode-toggled(); }
|
||||
}
|
||||
|
||||
Caption {
|
||||
|
||||
Reference in New Issue
Block a user