Face Recognition / People Integration #61
97
src/faces.rs
97
src/faces.rs
@@ -2107,6 +2107,8 @@ async fn update_face_handler<D: FaceDao>(
|
|||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
path: web::Path<i32>,
|
path: web::Path<i32>,
|
||||||
body: web::Json<UpdateFaceReq>,
|
body: web::Json<UpdateFaceReq>,
|
||||||
|
app_state: web::Data<AppState>,
|
||||||
|
face_client: web::Data<FaceClient>,
|
||||||
face_dao: web::Data<Mutex<D>>,
|
face_dao: web::Data<Mutex<D>>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let context = extract_context_from_request(&request);
|
let context = extract_context_from_request(&request);
|
||||||
@@ -2121,21 +2123,92 @@ async fn update_face_handler<D: FaceDao>(
|
|||||||
};
|
};
|
||||||
let bbox_patch = body.bbox.as_ref().map(|b| (b.x, b.y, b.w, b.h));
|
let bbox_patch = body.bbox.as_ref().map(|b| (b.x, b.y, b.w, b.h));
|
||||||
|
|
||||||
// bbox change → embedding becomes stale. Phase 2 only stores the new
|
// Bbox change → re-embed. The embedding is what auto-bind and the
|
||||||
// bbox; re-embed is a Phase 3 concern (it requires reading the image
|
// cluster suggester key on, so leaving it stale would silently
|
||||||
// off disk and going back through face_client.embed). For now log a
|
// corrupt every downstream similarity match. We crop the new bbox,
|
||||||
// warning so we can spot orphan-embedding rows.
|
// pass it through face_client.embed, and store the fresh vector.
|
||||||
if bbox_patch.is_some() {
|
// Net cost: one Apollo round-trip per bbox edit (~100-500ms on
|
||||||
warn!(
|
// CPU); acceptable for a manual operator action.
|
||||||
"PATCH /image/faces/{}: bbox updated; embedding now stale (Phase 3 will re-embed)",
|
let mut new_embedding: Option<Vec<u8>> = None;
|
||||||
id
|
if let Some((bx, by, bw, bh)) = bbox_patch {
|
||||||
);
|
if !face_client.is_enabled() {
|
||||||
|
return HttpResponse::ServiceUnavailable()
|
||||||
|
.body("face client disabled — bbox edit requires Apollo");
|
||||||
|
}
|
||||||
|
// Look up the current row so we know which photo to crop.
|
||||||
|
let current = {
|
||||||
|
let mut dao = face_dao.lock().expect("face dao lock");
|
||||||
|
match dao.get_face(&span_context, id) {
|
||||||
|
Ok(Some(r)) => r,
|
||||||
|
Ok(None) => return HttpResponse::NotFound().finish(),
|
||||||
|
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let library = match app_state.library_by_id(current.library_id) {
|
||||||
|
Some(l) => l.clone(),
|
||||||
|
None => {
|
||||||
|
return HttpResponse::InternalServerError().body(format!(
|
||||||
|
"face row references unknown library_id {}",
|
||||||
|
current.library_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let abs_path = library.resolve(¤t.rel_path);
|
||||||
|
let crop_bytes = match crop_image_to_bbox(&abs_path, bx, by, bw, bh) {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"PATCH /image/faces/{}: crop failed for {:?}: {:?}",
|
||||||
|
id, abs_path, e
|
||||||
|
);
|
||||||
|
return HttpResponse::BadRequest()
|
||||||
|
.body(format!("cannot crop new bbox: {}", e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let meta = DetectMeta {
|
||||||
|
content_hash: current.content_hash.clone(),
|
||||||
|
library_id: current.library_id,
|
||||||
|
rel_path: current.rel_path.clone(),
|
||||||
|
orientation: None,
|
||||||
|
model_version: Some(current.model_version.clone()),
|
||||||
|
};
|
||||||
|
match face_client.embed(crop_bytes, meta).await {
|
||||||
|
Ok(resp) => {
|
||||||
|
if let Some(face) = resp.faces.first() {
|
||||||
|
match face.decode_embedding() {
|
||||||
|
Ok(b) => new_embedding = Some(b),
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"PATCH /image/faces/{}: bad embedding from face service: {:?}",
|
||||||
|
id, e
|
||||||
|
);
|
||||||
|
return HttpResponse::BadGateway()
|
||||||
|
.body("invalid embedding from face service");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return HttpResponse::UnprocessableEntity()
|
||||||
|
.body("no face in new bbox");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(FaceDetectError::Permanent(e)) => {
|
||||||
|
return HttpResponse::UnprocessableEntity().body(format!("{}", e));
|
||||||
|
}
|
||||||
|
Err(FaceDetectError::Transient(e)) => {
|
||||||
|
return HttpResponse::ServiceUnavailable().body(format!("{}", e));
|
||||||
|
}
|
||||||
|
Err(FaceDetectError::Disabled) => {
|
||||||
|
return HttpResponse::ServiceUnavailable()
|
||||||
|
.body("face client disabled mid-flight");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut dao = face_dao.lock().expect("face dao lock");
|
let mut dao = face_dao.lock().expect("face dao lock");
|
||||||
dao.update_face(&span_context, id, person_patch, bbox_patch, None)
|
match dao.update_face(&span_context, id, person_patch, bbox_patch, new_embedding) {
|
||||||
.map(|row| HttpResponse::Ok().json(row))
|
Ok(row) => HttpResponse::Ok().json(row),
|
||||||
.into_http_internal_err()
|
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_face_handler<D: FaceDao>(
|
async fn delete_face_handler<D: FaceDao>(
|
||||||
|
|||||||
Reference in New Issue
Block a user