feat(ccip): schema for precomputed incremental character prototypes (#1317, m138 step 1)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 33s
CI / integration (push) Successful in 3m42s

Foundation for making CCIP character references a precomputed, INCREMENTAL
artifact instead of a request-path rebuild (kills the per-accept ~4s suggestions
stall; cost will scale with change, not library size):

- character_prototype: a character's reference CCIP vectors, capped to
  MLSettings.ccip_prototype_cap so match cost doesn't grow with popularity.
- ccip_prototype_state: per-character fingerprint (ref count + max region id) +
  updated_at → drives per-character incremental rebuilds and the matcher cache's
  reload-only-what-advanced.
- MLSettings.ccip_ref_signature (cheap global change gate) + ccip_prototype_cap.

Migration 0079. Schema + models only — the builder service, refresh task/beat,
and matcher rewrite land in the following steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-06 15:58:11 -04:00
parent 48be921b8d
commit f24dc81764
4 changed files with 154 additions and 0 deletions
+3
View File
@@ -5,6 +5,7 @@ from .artist import Artist
from .artist_visit import ArtistVisit
from .backup_run import BackupRun
from .base import Base
from .character_prototype import CcipPrototypeState, CharacterPrototype
from .credential import Credential
from .download_event import DownloadEvent
from .external_link import ExternalLink
@@ -79,6 +80,8 @@ __all__ = [
"HeadTrainingRun",
"TagAlias",
"TagHead",
"CharacterPrototype",
"CcipPrototypeState",
"TagPositiveConfirmation",
"TagSuggestionRejection",
"TaskRun",
+62
View File
@@ -0,0 +1,62 @@
"""Precomputed CCIP character prototypes (#1317, milestone 138).
The live matcher (ccip.match_image) needs each character's reference figure
vectors. Building that on the request path reloaded EVERY figure CCIP vector in
the library on any change (~4s, invalidated by every character accept). These
tables make the references a PRECOMPUTED, INCREMENTAL artifact refreshed off the
request path (services.ml.character_prototypes):
- CharacterPrototype: a character's reference vectors, capped to
MLSettings.ccip_prototype_cap so MATCH cost doesn't grow with a character's
popularity. The async matcher only READS these.
- CcipPrototypeState: a per-character fingerprint (reference count + max region
id) so a refresh rebuilds ONLY the characters whose references changed, and
its updated_at lets the matcher's cache reload just the advanced characters.
"""
from datetime import datetime
from pgvector.sqlalchemy import Vector
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from .base import Base
from .image_region import CCIP_DIM
class CharacterPrototype(Base):
__tablename__ = "character_prototype"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
# The character tag these vectors identify. CASCADE: deleting the tag drops
# its prototypes.
tag_id: Mapped[int] = mapped_column(
ForeignKey("tag.id", ondelete="CASCADE"), nullable=False, index=True
)
# A reference figure/face CCIP vector (same space as
# ImageRegion.ccip_embedding).
ccip_embedding: Mapped[list[float]] = mapped_column(
Vector(CCIP_DIM), nullable=False
)
# Provenance: the region this vector was copied from. SET NULL so pruning a
# region doesn't delete the prototype mid-cycle (the next refresh reconciles).
region_id: Mapped[int | None] = mapped_column(
ForeignKey("image_region.id", ondelete="SET NULL"), nullable=True
)
class CcipPrototypeState(Base):
__tablename__ = "ccip_prototype_state"
# One row per character that currently has prototypes.
tag_id: Mapped[int] = mapped_column(
ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True
)
# count(reference regions) + max(region id) at last build — the cheap
# per-character change detector that drives incremental rebuilds.
fingerprint: Mapped[str] = mapped_column(String(64), nullable=False)
# Bumped when this character's prototypes are rebuilt; the matcher cache
# reloads only characters whose updated_at advanced.
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
+12
View File
@@ -155,6 +155,18 @@ class MLSettings(Base):
detector_dedupe_iou: Mapped[float] = mapped_column(
Float, nullable=False, default=0.85
)
# -- CCIP character prototypes (#1317) ---------------------------------
# The per-character reference set is precomputed + refreshed INCREMENTALLY
# (services.ml.character_prototypes) instead of rebuilt on the request path.
# ccip_ref_signature is the cheap GLOBAL gate — when it's unchanged the
# refresh no-ops; ccip_prototype_cap bounds the reference vectors kept per
# character so MATCH cost doesn't grow with a character's popularity.
ccip_ref_signature: Mapped[str | None] = mapped_column(
String(128), nullable=True
)
ccip_prototype_cap: Mapped[int] = mapped_column(
Integer, nullable=False, default=64
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)