-
Notifications
You must be signed in to change notification settings - Fork 15
Add reviewable person identities that survive re-indexing (#85) #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from typing import Literal | ||
|
|
||
| from pydantic import ( | ||
| AwareDatetime, | ||
| BaseModel, | ||
| ConfigDict, | ||
| Field, | ||
| field_validator, | ||
| ) | ||
|
|
||
| from vidxp.core.identifiers import ( | ||
| ActorClusterId, | ||
| IndexGenerationId, | ||
| MediaId, | ||
| MimeType, | ||
| PersonId, | ||
| PersonReferenceId, | ||
| Sha256, | ||
| ) | ||
| from vidxp.core.storage_keys import validate_storage_key | ||
|
|
||
|
|
||
| PEOPLE_SCHEMA_VERSION = 1 | ||
|
|
||
|
|
||
| class _PersonModel(BaseModel): | ||
| model_config = ConfigDict( | ||
| extra="forbid", | ||
| frozen=True, | ||
| allow_inf_nan=False, | ||
| strict=True, | ||
| ) | ||
|
|
||
|
|
||
| class PersonRecord(_PersonModel): | ||
| """A repository-specific, user-reviewed person identity. | ||
|
|
||
| A PersonRecord is independent of any single index generation or | ||
| actor cluster: it represents a durable, user-approved identity | ||
| that survives re-indexing (see PersonClusterLink for the evidence | ||
| that ties it to specific anonymous face clusters). | ||
| """ | ||
|
|
||
| schema_version: Literal[PEOPLE_SCHEMA_VERSION] = PEOPLE_SCHEMA_VERSION | ||
| person_id: PersonId | ||
| display_name: str = Field(min_length=1, max_length=255) | ||
| notes: str | None = Field(default=None, max_length=10_000) | ||
| biography: str | None = Field(default=None, max_length=10_000) | ||
| created_at: AwareDatetime | ||
|
|
||
|
|
||
| class PersonAlias(_PersonModel): | ||
| """An alternate name a user has attached to a reviewed person.""" | ||
|
|
||
| person_id: PersonId | ||
| alias: str = Field(min_length=1, max_length=255) | ||
|
|
||
|
|
||
| class StagedPersonReference(_PersonModel): | ||
| """A reference image written to a temporary path, not yet published.""" | ||
|
|
||
| reference_id: PersonReferenceId | ||
| person_id: PersonId | ||
| path: Path | ||
|
|
||
|
|
||
| class StoredPersonReference(_PersonModel): | ||
| """A reference image that has been published into managed storage.""" | ||
|
|
||
| sha256: Sha256 | ||
| byte_size: int = Field(gt=0) | ||
| storage_key: str = Field(min_length=1) | ||
| local_path: Path | ||
|
|
||
| @field_validator("storage_key") | ||
| @classmethod | ||
| def _validate_storage_key(cls, value: str) -> str: | ||
| return validate_storage_key(value) | ||
|
|
||
|
|
||
| class PersonReference(_PersonModel): | ||
| """A user-supplied reference image attached to a reviewed person. | ||
|
|
||
| Deliberately independent of ArtifactRecord: artifacts are | ||
| job-generated, generation-scoped, and may expire, while a | ||
| reference image is durable, user-supplied, and outlives any | ||
| single index generation. | ||
| """ | ||
|
|
||
| reference_id: PersonReferenceId | ||
| person_id: PersonId | ||
| storage_key: str = Field(min_length=1) | ||
| sha256: Sha256 | ||
| byte_size: int = Field(gt=0) | ||
| mime_type: MimeType | ||
| created_at: AwareDatetime | ||
|
|
||
| @field_validator("storage_key") | ||
| @classmethod | ||
| def _validate_storage_key(cls, value: str) -> str: | ||
| return validate_storage_key(value) | ||
|
|
||
|
|
||
| class PersonClusterLink(_PersonModel): | ||
| """Evidence linking a reviewed person to an anonymous actor cluster. | ||
|
|
||
| The cluster_id is scoped to one media item and one index | ||
| generation: it is evidence, not identity. A person may accumulate | ||
| multiple links across different videos and across re-indexing | ||
| generations. Removing a link (to correct an accidental merge or | ||
| split) never touches the underlying video, media, or index data. | ||
| """ | ||
|
|
||
| person_id: PersonId | ||
| cluster_id: ActorClusterId | ||
| media_id: MediaId | ||
| generation_id: IndexGenerationId | ||
| created_at: AwareDatetime | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ def main(arguments: Sequence[str] | None = None) -> None: | |
| mode=ApplicationMode.server, | ||
| runtime_backend="cpu", | ||
| ) | ||
| settings.layout.ensure_local_directories() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we remove this change from the PR or add a focused test showing the failure it fixes?
|
||
| upgrade_database(workflow_database_url(settings)) | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PersonAlias,StagedPersonReference, andStoredPersonReferenceare not used by the implementation. The catalog accepts raw alias values, and no reference-image store produces the staged or stored reference types. This leaves two representations of the same data without a clear authority.Could we either wire these models through the catalog and a reference-image storage workflow in this PR, or remove them until that workflow is implemented? The storage workflow can reuse
LocalObjectStoreto publish, verify, resolve, and delete the image bytes rather than only recording caller-provided metadata.