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
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:
+22
-2
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user