faces: PATCH/POST /image/faces returns person_name with the row
Both create_face_handler and update_face_handler returned the bare
FaceDetectionRow, so PATCH /image/faces/{id} (used by both bbox edits
and person assignment) replied without person_name. The carousel
overlay does an optimistic replace on this row — replacing the joined
FaceWithPerson with a row that has person_name = undefined visibly
dropped the VFD label off the bbox after every save.
Add a small hydrate_face_with_person helper that does the persons
lookup and assembles a FaceWithPerson, used by both handlers. The
list endpoint already does the join, so the PATCH/POST shape now
matches it.
This commit is contained in:
46
src/faces.rs
46
src/faces.rs
@@ -117,6 +117,35 @@ struct InsertFaceDetection {
|
||||
created_at: i64,
|
||||
}
|
||||
|
||||
/// Build a [`FaceWithPerson`] from a freshly-mutated row by resolving the
|
||||
/// person name via [`FaceDao::get_person`]. Used by `create_face_handler`
|
||||
/// and `update_face_handler` so PATCH/POST responses match the join shape
|
||||
/// `/image/faces` returns — without this the carousel overlay's
|
||||
/// optimistic-replace would clobber the rendered name (the bare
|
||||
/// [`FaceDetectionRow`] doesn't carry it).
|
||||
fn hydrate_face_with_person<D: FaceDao>(
|
||||
dao: &mut D,
|
||||
ctx: &opentelemetry::Context,
|
||||
row: FaceDetectionRow,
|
||||
) -> anyhow::Result<FaceWithPerson> {
|
||||
let person_name = match row.person_id {
|
||||
Some(pid) => dao.get_person(ctx, pid)?.map(|p| p.name),
|
||||
None => None,
|
||||
};
|
||||
Ok(FaceWithPerson {
|
||||
id: row.id,
|
||||
bbox_x: row.bbox_x.unwrap_or(0.0),
|
||||
bbox_y: row.bbox_y.unwrap_or(0.0),
|
||||
bbox_w: row.bbox_w.unwrap_or(0.0),
|
||||
bbox_h: row.bbox_h.unwrap_or(0.0),
|
||||
confidence: row.confidence.unwrap_or(0.0),
|
||||
source: row.source,
|
||||
person_id: row.person_id,
|
||||
person_name,
|
||||
model_version: row.model_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// Face row decorated with its assigned person's name. Returned by
|
||||
/// `/image/faces` for the rendering side (carousel overlay, person chips).
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
@@ -2099,7 +2128,10 @@ async fn create_face_handler<D: FaceDao>(
|
||||
"Created manual face id={} library={} hash={} person_id={:?}",
|
||||
row.id, row.library_id, row.content_hash, row.person_id
|
||||
);
|
||||
HttpResponse::Created().json(row)
|
||||
match hydrate_face_with_person(&mut *dao, &span_context, row) {
|
||||
Ok(joined) => HttpResponse::Created().json(joined),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_face_handler<D: FaceDao>(
|
||||
@@ -2205,8 +2237,16 @@ async fn update_face_handler<D: FaceDao>(
|
||||
}
|
||||
|
||||
let mut dao = face_dao.lock().expect("face dao lock");
|
||||
match dao.update_face(&span_context, id, person_patch, bbox_patch, new_embedding) {
|
||||
Ok(row) => HttpResponse::Ok().json(row),
|
||||
let row = match dao.update_face(&span_context, id, person_patch, bbox_patch, new_embedding) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return HttpResponse::InternalServerError().body(e.to_string()),
|
||||
};
|
||||
// Hydrate person_name so the response shape matches GET /image/faces
|
||||
// — the carousel overlay does an optimistic replace on this row, and
|
||||
// a bare FaceDetectionRow with no person_name would visibly drop the
|
||||
// VFD label off the bbox even though the assignment didn't change.
|
||||
match hydrate_face_with_person(&mut *dao, &span_context, row) {
|
||||
Ok(joined) => HttpResponse::Ok().json(joined),
|
||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user