[pull] develop from photoprism:develop - #1775
Merged
Merged
Conversation
Adds the nullable subjects.subj_birthday column, created by GORM auto-migration, and surfaces it in the Edit Person dialog through Vuetify's date picker. It is an operator assertion like the verified flag: knowledge only a person has, which nothing in the pipeline can derive, and it is written by Subject.SaveForm alone. Subject.SetBirthday normalizes to UTC midnight, reading the calendar date in the submitted value's own location, because the column carries a time and a zone that a birthday does not have. Values outside BirthYearMin to today are rejected, with a day of headroom at the top for the eastern zones; the frontend mirrors the constant so the picker cannot offer a year the API refuses. Two places are hardened for a column that can be null, since every field in these projections could previously only be empty: - form.NewSubject gives the form its own copy of each pointer value, so binding a request writes into the form rather than back into the model it came from. - refetchResults in the people list treats null as a value rather than a missing field, so a cleared column reaches the loaded model. The Birthday label still has to reach translations.pot in a catalog commit.
Subject.SaveForm renames through UpdateName, which saves the row, renames every marker and flags the person's photos for re-check as it goes. Anything the form can reject is now checked ahead of it, so a request that is refused leaves the index as it found it rather than committing the rename and reporting failure. Also closes the gaps a review found in the date-of-birth tests, each of which would have stayed green through the regression it names: - The lower bound is pinned to its literal year on both sides, so moving the constant without moving the picker's copy fails. - The future-date headroom is measured in whole days; subtracting an hour landed back on today for runs starting before 01:00 UTC, where the case proved nothing. - Both projections are asserted to serialize the field as null rather than to leave the key out, which the client reads as "no such field" and never sends. - The frontend reads the day from the instant in UTC, so an offset rendering cannot move it, and the timezone helper restores an unset TZ by unsetting it.
search.UserSubjects is the session-scoped entry point the API uses, mirroring UserPhotos beside Photos, which stays unscoped for internal callers. A role the ACL denies AccessPrivate has private forced to no and all cleared, since all skips the visibility filters rather than relaxing one of them. The visibility and soft-delete filters apply to every branch of the query, including the uid lookup - that shortcut skips only the content filters, so reloading one person by uid returns that row whatever its file count. A subject a session may not read answers as not found. Rejected form values answer 400: SaveForm wraps them in the new entity.ErrInvalidValue and UpdateSubject maps it to a bad request, keeping a value the client has to correct out of the server-fault log. The people list carries its own hidden filter into the uid-scoped refetch, so the refetch asks the question the list asked. Its result-count notification is silenced for the tab that is not on screen: both tabs are mounted eagerly and search when they are created. The edit dialog reuses the existing Birth Date message rather than adding one.
A client session carries no user, so resolving a user role to scope the results answers RoleNone and narrows what the client role was authorized with. sessionGrantsPeople mirrors sessionGrantsPhotos: the client role must permit it, and a client without a user is evaluated on that role alone. SaveForm now splits validating a value from applying it, so a rename that merges this person into another leaves nothing assigned to an entity the handler serializes but never saves. NormalizeBirthday is the validating half. FindSubjectForSession is the shared lookup all four subject handlers use, so the read that refuses and the three writes cannot drift apart. A rejected value is logged at warn beside the 400, since AbortBadRequest reaches only the debug log. The tests that were missing are the point of the commit: each of the two defects above has one that fails without its fix, the handlers are pinned together, and the bound, the scoping predicate and the validating half are covered directly.
The flag had a definition, a writer in SaveForm and a session-scoped read path, but nothing that set it - so it did nothing for anyone who was not calling the API by hand. The checkbox is what makes it a feature. Placed beside Hidden, since both are visibility, and laid out 12 / 6 / 3 so the four short labels sit on one row on a desktop, two on a tablet and one per row on a phone. It reuses the existing Private message, so the catalog is unchanged.
Four across fits the English labels and not the translated ones - Verifiziert and Verborgen are half again as long as Verified and Hidden, in a dialog capped at 500px whatever the screen. Two per row from the small breakpoint up, one per row below it.
The column records how many embeddings were averaged into embedding_json, which is what says whether a centroid was computed at all. UpdateMatchStats had been writing the markers matched in a pass into it, so one field carried two meanings and a one-embedding cluster read as a grouping once it recruited. It keeps the radius ratchet and gives up the count; SetSampleRadius replaces SetMatchStats for the same reason, since measuring an extent over the members says nothing about the centroid, which it deliberately does not recompute. A single embedding also has no extent, so SetEmbeddings stores Epsilon where it stored the full cluster radius. A labeled example now reaches MatchDist and no further, rather than casting a cluster's width on the evidence of one photograph. face.ManualClusterCore is the number of embeddings that make a cluster: fewer are not merged, and are not offered for matching. Lower than ClusterCore because each manual face already asserts an identity, so the risk is not chance proximity in unlabeled data. Merging three produces a real centroid and a measured radius. Match ordering now counts members through the join FaceReports already uses, rather than reading samples for a question it does not answer. Tests that encoded the old rules are rewritten rather than relaxed, including a pair that links, waits, and completes in one pass when a third label arrives.
The audit repaired clusters stored without a radius by writing the full cluster radius, on the premise that no write path leaves a row at or below Epsilon. That premise no longer holds, and neither does the remedy: a singleton has no extent to repair, and a real cluster needs one measured over its members rather than a constant. audit --fix now recomputes from the markers a cluster holds and leaves one-embedding rows alone. face.RadiusFrom answers an unmeasurable spread with the floor for the same reason, so it agrees with SetEmbeddings again rather than with what SetEmbeddings used to do.
The README described the sample count as a match high-water mark and a cluster without a measurable extent as reaching the full cluster radius. Both moved, so the section that explained them is rewritten around what a single embedding actually is: a labeled example, with neither a centroid nor a radius, until a core of them merges into one.
The gate went in on the read side alone, so the paths that create and feed sub-threshold clusters kept running and left rows nothing could reach again. Face.MatchMarkers now refuses below two embeddings. It runs once, when a named marker gets a cluster, and the matcher never offers that cluster again - so a labeled example left free to adopt there kept exactly the reach the gate removes. The migration stops rebuilding a subject from a single embedding, and names the subjects it skipped, since "this person stopped being recognized after I switched models" is otherwise undiagnosable. face-cluster-core is bounded below at 2, which a value of 1 would otherwise turn into a library of clusters no pass offers. Matching admits a pair. One embedding is not a centroid, it is the embedding; two are already an average, and a pair that exists is worth using even though ManualClusterCore still refuses to make one. That keeps existing two-face clusters working rather than requiring a merge that may never come. Ordering by a joined member count is reverted. It cost a full scan of the markers table with a LENGTH() on the embedding blob, on the query the matcher runs twice per wake and the collision pass re-runs after every resolution - roughly twenty times the old cost, for an ordering that only decides how early a winner is met. It also sorted brand-new clusters last, which is the wrong end. Restores the audit coverage the rewrite dropped - the scoping clauses, the floor boundary and that Audit still calls the measurement - and replaces four assertions that passed for the wrong reason, each confirmed by mutation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )