Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/vidxp/core/identifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ def _require_workflow_uuid(value: str) -> str:
ArtifactId: TypeAlias = Uuid4Hex
UploadIntentId: TypeAlias = Uuid4Hex
UploadSessionId: TypeAlias = Uuid4Hex
PersonId: TypeAlias = Uuid4Hex
PersonReferenceId: TypeAlias = Uuid4Hex
121 changes: 121 additions & 0 deletions src/vidxp/core/people.py
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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersonAlias, StagedPersonReference, and StoredPersonReference are 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 LocalObjectStore to publish, verify, resolve, and delete the image bytes rather than only recording caller-provided metadata.

"""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
1 change: 1 addition & 0 deletions src/vidxp/database_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def main(arguments: Sequence[str] | None = None) -> None:
mode=ApplicationMode.server,
runtime_backend="cpu",
)
settings.layout.ensure_local_directories()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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?

main() selects server mode and therefore uses the bundled PostgreSQL URL, so creating local repository directories does not appear to address an SQLite parent-directory error. Keeping this separate would make the people change easier to review and avoid an unrelated filesystem side effect.

upgrade_database(workflow_database_url(settings))


Expand Down
12 changes: 10 additions & 2 deletions src/vidxp/infrastructure/local_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@
media,
media_import_requests,
metadata,
people,
person_aliases,
person_cluster_links,
person_references,
upload_intents,
upload_quota,
upload_session_files,
upload_sessions,
)

CATALOG_SCHEMA_VERSION = 4
CATALOG_SCHEMA_VERSION = 5
_local_metadata = MetaData()
catalog_metadata = Table(
"catalog_metadata",
Expand Down Expand Up @@ -46,6 +50,10 @@ def __init__(self, database: Path) -> None:
upload_sessions,
upload_session_files,
upload_quota,
people,
person_aliases,
person_references,
person_cluster_links,
),
)
_local_metadata.create_all(self.engine)
Expand All @@ -59,7 +67,7 @@ def __init__(self, database: Path) -> None:
schema_version=CATALOG_SCHEMA_VERSION
)
)
elif version in {1, 2, 3}:
elif version in {1, 2, 3, 4}:
connection.execute(
update(catalog_metadata).values(
schema_version=CATALOG_SCHEMA_VERSION
Expand Down
Loading