use serde::{Deserialize, Serialize};
use crate::{
validate_pose, CapturedPhotoAnalysis, Embedding, FaceAnalysis, FaceVerificationError,
LivenessChallenge, VerificationThresholds, EXPECTED_PHOTO_COUNT,
};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VerificationChecks {
pub all_have_face: bool,
pub same_person: bool,
pub poses_distinct: bool,
pub no_duplicates: bool,
pub pose_valid: bool,
pub selfie_safe: bool,
pub failed_index: Option<usize>,
}
impl Default for VerificationChecks {
fn default() -> Self {
Self {
all_have_face: true,
same_person: true,
poses_distinct: true,
no_duplicates: true,
pose_valid: true,
selfie_safe: true,
failed_index: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VerificationResult {
pub ok: bool,
pub embedding: Option<Embedding>,
pub estimated_age: Option<u8>,
pub error: Option<String>,
pub checks: VerificationChecks,
}
impl VerificationResult {
fn ok(embedding: Embedding, estimated_age: Option<u8>) -> Self {
Self {
ok: true,
embedding: Some(embedding),
estimated_age,
error: None,
checks: VerificationChecks::default(),
}
}
fn fail(error: impl Into<String>, checks: VerificationChecks) -> Self {
Self {
ok: false,
embedding: None,
estimated_age: None,
error: Some(error.into()),
checks,
}
}
}
pub fn verify_captured_photos(
photos: &[CapturedPhotoAnalysis],
challenge: &LivenessChallenge,
thresholds: &VerificationThresholds,
) -> VerificationResult {
if photos.len() != EXPECTED_PHOTO_COUNT {
return VerificationResult::fail("se esperan 6 fotos", VerificationChecks::default());
}
let mut hashes = std::collections::HashSet::new();
if !photos
.iter()
.all(|photo| hashes.insert(photo.hash.as_str()))
{
return VerificationResult::fail(
"Se detectaron fotos duplicadas. Repite el proceso.",
VerificationChecks {
no_duplicates: false,
..VerificationChecks::default()
},
);
}
if let Some(index) = photos.iter().position(|photo| !photo.frame.face.detected) {
return VerificationResult::fail(
format!("No se detectó tu cara en la foto {}.", index + 1),
VerificationChecks {
all_have_face: false,
failed_index: Some(index),
..VerificationChecks::default()
},
);
}
let embeddings = match collect_embeddings(photos) {
Ok(embeddings) => embeddings,
Err(index) => {
return VerificationResult::fail(
format!("No se pudo calcular el embedding de la foto {}.", index + 1),
VerificationChecks {
all_have_face: false,
failed_index: Some(index),
..VerificationChecks::default()
},
)
}
};
let distances = match embeddings[1..]
.iter()
.map(|embedding| face_distance(&embeddings[0], embedding))
.collect::<Result<Vec<_>, _>>()
{
Ok(distances) => distances,
Err(_) => {
return VerificationResult::fail(
"Los embeddings no tienen dimensiones compatibles.",
VerificationChecks {
same_person: false,
..VerificationChecks::default()
},
)
}
};
if !distances.iter().all(|d| *d < thresholds.same_person_max) {
return VerificationResult::fail(
"Las fotos no parecen ser de la misma persona.",
VerificationChecks {
same_person: false,
..VerificationChecks::default()
},
);
}
let min_dist = distances.iter().copied().fold(f32::INFINITY, f32::min);
let max_dist = distances.iter().copied().fold(f32::NEG_INFINITY, f32::max);
if max_dist - min_dist <= thresholds.poses_distinct_delta {
return VerificationResult::fail(
"Las fotos parecen demasiado similares. Asegúrate de seguir cada instrucción.",
VerificationChecks {
poses_distinct: false,
..VerificationChecks::default()
},
);
}
for (index, (photo, step)) in photos.iter().zip(challenge.steps.iter()).enumerate() {
let pose = validate_pose(step, &photo.frame, challenge, thresholds);
if !pose.valid {
return VerificationResult::fail(
format!(
"Foto {}: {} Repite la verificación.",
index + 1,
pose.message
),
VerificationChecks {
pose_valid: false,
failed_index: Some(index),
..VerificationChecks::default()
},
);
}
}
let estimated_age = estimated_age(&photos[0].frame.face, &photos[1].frame.face);
VerificationResult::ok(embeddings[0].clone(), estimated_age)
}
fn collect_embeddings(photos: &[CapturedPhotoAnalysis]) -> Result<Vec<Embedding>, usize> {
photos
.iter()
.enumerate()
.map(|(index, photo)| {
photo
.frame
.face
.embedding
.clone()
.filter(|embedding| !embedding.is_empty())
.ok_or(index)
})
.collect()
}
fn estimated_age(a: &FaceAnalysis, b: &FaceAnalysis) -> Option<u8> {
Some(((a.age? + b.age?) / 2.0).round().clamp(0.0, u8::MAX as f32) as u8)
}
pub fn face_distance(a: &[f32], b: &[f32]) -> Result<f32, FaceVerificationError> {
if a.len() != b.len() || a.is_empty() {
return Err(FaceVerificationError::InvalidEmbedding);
}
let sum = a
.iter()
.zip(b.iter())
.map(|(a, b)| {
let d = a - b;
d * d
})
.sum::<f32>();
Ok(sum.sqrt())
}