Tighten Auto-Match refs and log accept/reject (Phase 2).
CI / skip-ci-check (pull_request) Successful in 30s
CI / python-lint (pull_request) Successful in 31s
CI / docker-ci (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 35s
CI / e2e (pull_request) Successful in 2m34s
CI / viewer-unit (pull_request) Successful in 2m53s
CI / admin-unit (pull_request) Successful in 3m8s

Raise reference quality floor to 0.5, align browse/run/auto-accept defaults with UI copy, and persist match_decisions for later confidence calibration.
This commit is contained in:
2026-08-05 11:23:24 -04:00
parent dfd0c582e6
commit 085b7962cf
13 changed files with 485 additions and 87 deletions
+22 -2
View File
@@ -49,6 +49,7 @@ from backend.services.face_service import (
find_similar_faces,
get_auto_match_people_list,
list_unidentified_faces,
record_match_decisions,
)
from backend.services.face_service import (
get_auto_match_person_matches as get_person_matches_service,
@@ -612,7 +613,7 @@ def auto_match_faces(
"""Start auto-match process with tolerance threshold and optional auto-acceptance.
Matches desktop auto-match workflow exactly:
1. Gets all identified people (one face per person, best quality >= 0.3)
1. Gets all identified people (one face per person, best quality >= MIN_AUTO_MATCH_REFERENCE_QUALITY)
2. For each person, finds similar unidentified faces (confidence >= 40%)
3. Returns matches grouped by person, sorted by person name
@@ -620,7 +621,7 @@ def auto_match_faces(
- Only processes persons with frontal or tilted reference faces (not profile)
- Only processes persons with reference face quality > 50% (quality_score > 0.5)
- Only matches with frontal or tilted unidentified faces (not profile)
- Only auto-accepts matches with similarity >= threshold
- Only auto-accepts matches with similarity >= threshold (default 85%)
- Only auto-accepts faces with quality > 50% (quality_score > 0.5)
"""
from backend.db.models import Person, Photo
@@ -649,6 +650,7 @@ def auto_match_faces(
# 3. Quality must be > 50% (quality_score > 0.5)
qualifying_faces = []
accept_log_items = []
for face, distance, confidence_pct in similar_faces:
# Check similarity threshold
if confidence_pct < request.auto_accept_threshold:
@@ -662,6 +664,14 @@ def auto_match_faces(
continue
qualifying_faces.append(face.id)
accept_log_items.append(
{
"face_id": face.id,
"similarity": float(confidence_pct),
"distance": float(distance),
"reference_face_id": reference_face_id,
}
)
# Auto-accept qualifying faces
if qualifying_faces:
@@ -670,6 +680,16 @@ def auto_match_faces(
db, person_id, qualifying_faces
)
auto_accepted_faces += identified_count
if identified_count:
record_match_decisions(
db,
decision="accept",
source="auto_accept",
person_id=person_id,
items=accept_log_items,
user_id=None,
commit=True,
)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
+58 -4
View File
@@ -21,7 +21,7 @@ from backend.schemas.people import (
PersonUpdateRequest,
PersonWithFacesResponse,
)
from backend.services.face_service import accept_auto_match_matches
from backend.services.face_service import accept_auto_match_matches, record_match_decisions
router = APIRouter(prefix="/people", tags=["people"])
@@ -278,9 +278,63 @@ def accept_matches(
user_id = current_user["user_id"]
try:
identified_count, updated_count = accept_auto_match_matches(
db, person_id, request.face_ids, user_id=user_id
)
person = db.query(Person).filter(Person.id == person_id).first()
if not person:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Person {person_id} not found",
)
if request.face_ids:
accept_auto_match_matches(
db, person_id, request.face_ids, user_id=user_id
)
# Log accepts (with optional scores from client)
score_by_face = {
item.face_id: item for item in (request.accepted_matches or [])
}
accept_items = []
for face_id in request.face_ids:
scored = score_by_face.get(face_id)
accept_items.append(
{
"face_id": face_id,
"similarity": scored.similarity if scored else None,
"distance": scored.distance if scored else None,
"reference_face_id": scored.reference_face_id if scored else None,
}
)
if accept_items:
record_match_decisions(
db,
decision="accept",
source="auto_match",
person_id=person_id,
items=accept_items,
user_id=user_id,
commit=True,
)
if request.rejected_matches:
reject_items = [
{
"face_id": item.face_id,
"similarity": item.similarity,
"distance": item.distance,
"reference_face_id": item.reference_face_id,
}
for item in request.rejected_matches
]
record_match_decisions(
db,
decision="reject",
source="auto_match",
person_id=person_id,
items=reject_items,
user_id=user_id,
commit=True,
)
except ValueError as e:
if "not found" in str(e).lower():
raise HTTPException(