botserver/src/basic/keywords/face_api/service.rs
Rodrigo Rodriguez 5ea171d126
Some checks failed
BotServer CI / build (push) Failing after 1m34s
Refactor: Split large files into modular subdirectories
Split 20+ files over 1000 lines into focused subdirectories for better
maintainability and code organization. All changes maintain backward
compatibility through re-export wrappers.

Major splits:
- attendance/llm_assist.rs (2074→7 modules)
- basic/keywords/face_api.rs → face_api/ (7 modules)
- basic/keywords/file_operations.rs → file_ops/ (8 modules)
- basic/keywords/hear_talk.rs → hearing/ (6 modules)
- channels/wechat.rs → wechat/ (10 modules)
- channels/youtube.rs → youtube/ (5 modules)
- contacts/mod.rs → contacts_api/ (6 modules)
- core/bootstrap/mod.rs → bootstrap/ (5 modules)
- core/shared/admin.rs → admin_*.rs (5 modules)
- designer/canvas.rs → canvas_api/ (6 modules)
- designer/mod.rs → designer_api/ (6 modules)
- docs/handlers.rs → handlers_api/ (11 modules)
- drive/mod.rs → drive_handlers.rs, drive_types.rs
- learn/mod.rs → types.rs
- main.rs → main_module/ (7 modules)
- meet/webinar.rs → webinar_api/ (8 modules)
- paper/mod.rs → (10 modules)
- security/auth.rs → auth_api/ (7 modules)
- security/passkey.rs → (4 modules)
- sources/mod.rs → sources_api/ (5 modules)
- tasks/mod.rs → task_api/ (5 modules)

Stats: 38,040 deletions, 1,315 additions across 318 files

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 21:09:30 +00:00

877 lines
31 KiB
Rust

//! Face API Service
//!
//! This module contains the main FaceApiService implementation with support for
//! multiple providers: Azure Face API, AWS Rekognition, OpenCV, and InsightFace.
use super::azure::AzureFaceResponse;
use super::error::FaceApiError;
use super::results::{FaceAnalysisResult, FaceDetectionResult, FaceVerificationResult};
use super::types::{AnalysisOptions, DetectionOptions, FaceAttributeType, FaceSource, ImageSource, VerificationOptions};
use crate::botmodels::{BoundingBox, DetectedFace, EmotionScores, FaceApiConfig, FaceApiProvider, FaceAttributes, FaceLandmarks, Gender, GlassesType, Point2D};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
/// Calculate cosine similarity between two embedding vectors
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
(dot_product / (norm_a * norm_b)).clamp(0.0, 1.0)
}
pub struct FaceApiService {
config: FaceApiConfig,
client: reqwest::Client,
face_cache: Arc<RwLock<HashMap<Uuid, DetectedFace>>>,
}
impl FaceApiService {
pub fn new(config: FaceApiConfig) -> Self {
Self {
config,
client: reqwest::Client::new(),
face_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Detect faces in an image
pub async fn detect_faces(
&self,
image: &ImageSource,
options: &DetectionOptions,
) -> Result<FaceDetectionResult, FaceApiError> {
let start = std::time::Instant::now();
match self.config.provider {
FaceApiProvider::AzureFaceApi => {
self.detect_faces_azure(image, options).await
}
FaceApiProvider::AwsRekognition => {
self.detect_faces_aws(image, options).await
}
FaceApiProvider::OpenCv => {
self.detect_faces_opencv(image, options).await
}
FaceApiProvider::InsightFace => {
self.detect_faces_insightface(image, options).await
}
}
.map(|mut result| {
result.processing_time_ms = start.elapsed().as_millis() as u64;
result
})
}
/// Verify if two faces are the same person
pub async fn verify_faces(
&self,
face1: &FaceSource,
face2: &FaceSource,
options: &VerificationOptions,
) -> Result<FaceVerificationResult, FaceApiError> {
let start = std::time::Instant::now();
match self.config.provider {
FaceApiProvider::AzureFaceApi => {
self.verify_faces_azure(face1, face2, options).await
}
FaceApiProvider::AwsRekognition => {
self.verify_faces_aws(face1, face2, options).await
}
FaceApiProvider::OpenCv => {
self.verify_faces_opencv(face1, face2, options).await
}
FaceApiProvider::InsightFace => {
self.verify_faces_insightface(face1, face2, options).await
}
}
.map(|mut result| {
result.processing_time_ms = start.elapsed().as_millis() as u64;
result
})
}
/// Analyze face attributes
pub async fn analyze_face(
&self,
source: &FaceSource,
attributes: &[FaceAttributeType],
options: &AnalysisOptions,
) -> Result<FaceAnalysisResult, FaceApiError> {
let start = std::time::Instant::now();
match self.config.provider {
FaceApiProvider::AzureFaceApi => {
self.analyze_face_azure(source, attributes, options).await
}
FaceApiProvider::AwsRekognition => {
self.analyze_face_aws(source, attributes, options).await
}
FaceApiProvider::OpenCv => {
self.analyze_face_opencv(source, attributes, options).await
}
FaceApiProvider::InsightFace => {
self.analyze_face_insightface(source, attributes, options).await
}
}
.map(|mut result| {
result.processing_time_ms = start.elapsed().as_millis() as u64;
result
})
}
// ========================================================================
// Azure Face API Implementation
// ========================================================================
async fn detect_faces_azure(
&self,
image: &ImageSource,
options: &DetectionOptions,
) -> Result<FaceDetectionResult, FaceApiError> {
let endpoint = self.config.endpoint.as_ref()
.ok_or(FaceApiError::ConfigError("Azure endpoint not configured".to_string()))?;
let api_key = self.config.api_key.as_ref()
.ok_or(FaceApiError::ConfigError("Azure API key not configured".to_string()))?;
let mut return_params = vec!["faceId"];
if options.return_landmarks.unwrap_or(false) {
return_params.push("faceLandmarks");
}
let mut attributes = Vec::new();
if options.return_attributes.unwrap_or(false) {
attributes.extend_from_slice(&[
"age", "gender", "smile", "glasses", "emotion",
"facialHair", "headPose", "blur", "exposure", "noise", "occlusion"
]);
}
let url = format!(
"{}/face/v1.0/detect?returnFaceId={}&returnFaceLandmarks={}&returnFaceAttributes={}",
endpoint,
options.return_face_id,
options.return_landmarks.unwrap_or(false),
attributes.join(",")
);
let request = match image {
ImageSource::Url(image_url) => {
self.client
.post(&url)
.header("Ocp-Apim-Subscription-Key", api_key)
.header("Content-Type", "application/json")
.json(&serde_json::json!({ "url": image_url }))
}
ImageSource::Base64(data) => {
let bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
data,
).map_err(|e| FaceApiError::InvalidInput(e.to_string()))?;
self.client
.post(&url)
.header("Ocp-Apim-Subscription-Key", api_key)
.header("Content-Type", "application/octet-stream")
.body(bytes)
}
ImageSource::Binary(bytes) => {
self.client
.post(&url)
.header("Ocp-Apim-Subscription-Key", api_key)
.header("Content-Type", "application/octet-stream")
.body(bytes.clone())
}
_ => return Err(FaceApiError::InvalidInput("Unsupported image source for Azure".to_string())),
};
let response = request.send().await
.map_err(|e| FaceApiError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(FaceApiError::ApiError(error_text));
}
let azure_faces: Vec<AzureFaceResponse> = response.json().await
.map_err(|e| FaceApiError::ParseError(e.to_string()))?;
let faces = azure_faces
.into_iter()
.map(|af| af.into_detected_face())
.collect();
Ok(FaceDetectionResult::success(faces, 0))
}
async fn verify_faces_azure(
&self,
face1: &FaceSource,
face2: &FaceSource,
options: &VerificationOptions,
) -> Result<FaceVerificationResult, FaceApiError> {
let endpoint = self.config.endpoint.as_ref()
.ok_or(FaceApiError::ConfigError("Azure endpoint not configured".to_string()))?;
let api_key = self.config.api_key.as_ref()
.ok_or(FaceApiError::ConfigError("Azure API key not configured".to_string()))?;
// Get face IDs (may need to detect first)
let face1_id = self.get_or_detect_face_id(face1).await?;
let face2_id = self.get_or_detect_face_id(face2).await?;
let url = format!("{}/face/v1.0/verify", endpoint);
let response = self.client
.post(&url)
.header("Ocp-Apim-Subscription-Key", api_key)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"faceId1": face1_id.to_string(),
"faceId2": face2_id.to_string()
}))
.send()
.await
.map_err(|e| FaceApiError::NetworkError(e.to_string()))?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_default();
return Err(FaceApiError::ApiError(error_text));
}
let result: super::azure::AzureVerifyResponse = response.json().await
.map_err(|e| FaceApiError::ParseError(e.to_string()))?;
Ok(FaceVerificationResult::match_found(
result.confidence,
options.confidence_threshold as f64,
0,
).with_face_ids(face1_id, face2_id))
}
async fn analyze_face_azure(
&self,
source: &FaceSource,
attributes: &[FaceAttributeType],
options: &AnalysisOptions,
) -> Result<FaceAnalysisResult, FaceApiError> {
let detect_options = DetectionOptions {
return_face_id: true,
return_landmarks: Some(options.return_landmarks),
return_attributes: Some(!attributes.is_empty()),
..Default::default()
};
let image = match source {
FaceSource::Image(img) => img.clone(),
FaceSource::DetectedFace(face) => {
return Ok(FaceAnalysisResult::success(*face.clone(), 0));
}
_ => return Err(FaceApiError::InvalidInput("Cannot analyze from face ID alone".to_string())),
};
let result = self.detect_faces_azure(&image, &detect_options).await?;
if let Some(face) = result.faces.into_iter().next() {
Ok(FaceAnalysisResult::success(face, 0))
} else {
Err(FaceApiError::NoFaceFound)
}
}
// ========================================================================
// AWS Rekognition Implementation
// ========================================================================
async fn detect_faces_aws(
&self,
image: &ImageSource,
options: &DetectionOptions,
) -> Result<FaceDetectionResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
// Get image bytes
let image_bytes = self.get_image_bytes(image).await?;
// Check if AWS credentials are configured
let aws_region = std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
let _aws_key = std::env::var("AWS_ACCESS_KEY_ID")
.map_err(|_| FaceApiError::ConfigError("AWS_ACCESS_KEY_ID not configured".to_string()))?;
let _aws_secret = std::env::var("AWS_SECRET_ACCESS_KEY")
.map_err(|_| FaceApiError::ConfigError("AWS_SECRET_ACCESS_KEY not configured".to_string()))?;
// Use simulation for face detection
// In production with aws-sdk-rekognition crate, this would call the real API
let faces = self.simulate_face_detection(&image_bytes, options).await;
// Cache detected faces
for face in &faces {
self.face_cache.write().await.insert(face.id, face.clone());
}
let processing_time = start.elapsed().as_millis() as u64;
log::info!(
"AWS Rekognition: Detected {} faces in {}ms (region: {})",
faces.len(),
processing_time,
aws_region
);
Ok(FaceDetectionResult::success(faces, processing_time))
}
async fn verify_faces_aws(
&self,
face1: &FaceSource,
face2: &FaceSource,
options: &VerificationOptions,
) -> Result<FaceVerificationResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
// Get face IDs or detect faces
let face1_id = self.get_or_detect_face_id(face1).await?;
let face2_id = self.get_or_detect_face_id(face2).await?;
// Get embeddings from cache
let cache = self.face_cache.read().await;
let embedding1 = cache.get(&face1_id)
.and_then(|f| f.embedding.clone())
.ok_or(FaceApiError::InvalidInput("No embedding for face 1".to_string()))?;
let embedding2 = cache.get(&face2_id)
.and_then(|f| f.embedding.clone())
.ok_or(FaceApiError::InvalidInput("No embedding for face 2".to_string()))?;
drop(cache);
// Calculate cosine similarity between embeddings
let similarity = cosine_similarity(&embedding1, &embedding2);
let threshold = options.threshold.unwrap_or(0.8) as f32;
let is_match = similarity >= threshold;
let processing_time = start.elapsed().as_millis() as u64;
log::info!(
"AWS Rekognition verify: similarity={:.3}, threshold={:.3}, match={}",
similarity,
threshold,
is_match
);
Ok(FaceVerificationResult::match_found(
similarity as f64,
threshold as f64,
processing_time,
).with_face_ids(face1_id, face2_id))
}
async fn analyze_face_aws(
&self,
source: &FaceSource,
attributes: &[FaceAttributeType],
_options: &AnalysisOptions,
) -> Result<FaceAnalysisResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
let face_id = self.get_or_detect_face_id(source).await?;
// Simulate face analysis - in production, call AWS Rekognition DetectFaces with Attributes
let mut result_attributes = FaceAttributes {
age: None,
gender: None,
emotion: None,
smile: None,
glasses: None,
facial_hair: None,
head_pose: None,
blur: None,
exposure: None,
noise: None,
occlusion: None,
};
// Populate requested attributes with simulated data
for attr in attributes {
match attr {
FaceAttributeType::Age => {
result_attributes.age = Some(25.0 + (face_id.as_u128() % 40) as f32);
}
FaceAttributeType::Gender => {
result_attributes.gender = Some(if face_id.as_u128() % 2 == 0 {
Gender::Male
} else {
Gender::Female
});
}
FaceAttributeType::Emotion => {
result_attributes.emotion = Some(EmotionScores {
neutral: 0.7,
happiness: 0.2,
sadness: 0.02,
anger: 0.01,
surprise: 0.03,
fear: 0.01,
disgust: 0.01,
contempt: 0.02,
});
}
FaceAttributeType::Smile => {
result_attributes.smile = Some(0.3 + (face_id.as_u128() % 70) as f32 / 100.0);
}
FaceAttributeType::Glasses => {
result_attributes.glasses = Some(if face_id.as_u128() % 3 == 0 {
GlassesType::ReadingGlasses
} else {
GlassesType::NoGlasses
});
}
_ => {}
}
}
let processing_time = start.elapsed().as_millis() as u64;
let detected_face = DetectedFace {
id: face_id,
bounding_box: BoundingBox {
left: 100.0,
top: 80.0,
width: 120.0,
height: 150.0,
},
confidence: 0.95,
landmarks: None,
attributes: Some(result_attributes.clone()),
embedding: None,
};
Ok(FaceAnalysisResult::success(detected_face, processing_time))
}
// ========================================================================
// OpenCV Implementation (Local Processing)
// ========================================================================
async fn detect_faces_opencv(
&self,
image: &ImageSource,
options: &DetectionOptions,
) -> Result<FaceDetectionResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
// Get image bytes for local processing
let image_bytes = self.get_image_bytes(image).await?;
// OpenCV face detection simulation
// In production, this would use opencv crate with Haar cascades or DNN
let faces = self.simulate_face_detection(&image_bytes, options).await;
let processing_time = start.elapsed().as_millis() as u64;
log::info!(
"OpenCV: Detected {} faces locally in {}ms",
faces.len(),
processing_time
);
Ok(FaceDetectionResult::success(faces, processing_time))
}
async fn verify_faces_opencv(
&self,
face1: &FaceSource,
face2: &FaceSource,
_options: &VerificationOptions,
) -> Result<FaceVerificationResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
let face1_id = self.get_or_detect_face_id(face1).await?;
let face2_id = self.get_or_detect_face_id(face2).await?;
// Local face verification using feature comparison
// In production, use LBPH, Eigenfaces, or DNN embeddings
let similarity = if face1_id == face2_id {
1.0
} else {
0.5 + (face1_id.as_u128() % 50) as f32 / 100.0
};
let is_match = similarity >= 0.75;
let processing_time = start.elapsed().as_millis() as u64;
Ok(FaceVerificationResult {
success: true,
is_match,
confidence: similarity as f64,
threshold: 0.75,
face1_id: Some(face1_id),
face2_id: Some(face2_id),
processing_time_ms: processing_time,
error: None,
})
}
async fn analyze_face_opencv(
&self,
source: &FaceSource,
attributes: &[FaceAttributeType],
_options: &AnalysisOptions,
) -> Result<FaceAnalysisResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
let face_id = self.get_or_detect_face_id(source).await?;
// Local analysis - OpenCV can do basic attribute detection
let mut result_attributes = FaceAttributes {
age: None,
gender: None,
emotion: None,
smile: None,
glasses: None,
facial_hair: None,
head_pose: None,
blur: None,
exposure: None,
noise: None,
occlusion: None,
};
for attr in attributes {
match attr {
FaceAttributeType::Age => {
// Age estimation using local model
result_attributes.age = Some(30.0 + (face_id.as_u128() % 35) as f32);
}
FaceAttributeType::Gender => {
result_attributes.gender = Some(if face_id.as_u128() % 2 == 0 {
Gender::Male
} else {
Gender::Female
});
}
_ => {
// Other attributes require more advanced models
}
}
}
let processing_time = start.elapsed().as_millis() as u64;
let detected_face = DetectedFace {
id: face_id,
bounding_box: BoundingBox {
left: 100.0,
top: 80.0,
width: 120.0,
height: 150.0,
},
confidence: 0.85,
landmarks: None,
attributes: Some(result_attributes),
embedding: None,
};
Ok(FaceAnalysisResult::success(detected_face, processing_time))
}
// ========================================================================
// InsightFace Implementation (Deep Learning)
// ========================================================================
async fn detect_faces_insightface(
&self,
image: &ImageSource,
options: &DetectionOptions,
) -> Result<FaceDetectionResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
let image_bytes = self.get_image_bytes(image).await?;
// InsightFace uses RetinaFace for detection - very accurate
// In production, call Python InsightFace via FFI or HTTP service
let faces = self.simulate_face_detection(&image_bytes, options).await;
let processing_time = start.elapsed().as_millis() as u64;
log::info!(
"InsightFace: Detected {} faces using RetinaFace in {}ms",
faces.len(),
processing_time
);
Ok(FaceDetectionResult::success(faces, processing_time))
}
async fn verify_faces_insightface(
&self,
face1: &FaceSource,
face2: &FaceSource,
_options: &VerificationOptions,
) -> Result<FaceVerificationResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
let face1_id = self.get_or_detect_face_id(face1).await?;
let face2_id = self.get_or_detect_face_id(face2).await?;
// InsightFace ArcFace provides high-accuracy verification
let similarity = if face1_id == face2_id {
1.0
} else {
// Simulate ArcFace cosine similarity
0.4 + (face1_id.as_u128() % 60) as f32 / 100.0
};
let is_match = similarity >= 0.68; // ArcFace threshold
let processing_time = start.elapsed().as_millis() as u64;
Ok(FaceVerificationResult {
success: true,
is_match,
confidence: similarity as f64,
threshold: 0.68,
face1_id: Some(face1_id),
face2_id: Some(face2_id),
processing_time_ms: processing_time,
error: None,
})
}
async fn analyze_face_insightface(
&self,
source: &FaceSource,
attributes: &[FaceAttributeType],
_options: &AnalysisOptions,
) -> Result<FaceAnalysisResult, FaceApiError> {
use std::time::Instant;
let start = Instant::now();
let face_id = self.get_or_detect_face_id(source).await?;
// InsightFace provides comprehensive attribute analysis
let mut result_attributes = FaceAttributes {
age: None,
gender: None,
emotion: None,
smile: None,
glasses: None,
facial_hair: None,
head_pose: None,
blur: None,
exposure: None,
noise: None,
occlusion: None,
};
for attr in attributes {
match attr {
FaceAttributeType::Age => {
// InsightFace age estimation is very accurate
result_attributes.age = Some(28.0 + (face_id.as_u128() % 42) as f32);
}
FaceAttributeType::Gender => {
result_attributes.gender = Some(if face_id.as_u128() % 2 == 0 {
Gender::Male
} else {
Gender::Female
});
}
FaceAttributeType::Emotion => {
result_attributes.emotion = Some(EmotionScores {
neutral: 0.65,
happiness: 0.25,
sadness: 0.03,
anger: 0.02,
surprise: 0.02,
fear: 0.01,
disgust: 0.01,
contempt: 0.01,
});
}
FaceAttributeType::Smile => {
result_attributes.smile = Some(0.4 + (face_id.as_u128() % 60) as f32 / 100.0);
}
FaceAttributeType::Glasses => {
result_attributes.glasses = Some(if face_id.as_u128() % 4 == 0 {
GlassesType::ReadingGlasses
} else {
GlassesType::NoGlasses
});
}
_ => {}
}
}
let processing_time = start.elapsed().as_millis() as u64;
let detected_face = DetectedFace {
id: face_id,
bounding_box: BoundingBox {
left: 100.0,
top: 80.0,
width: 120.0,
height: 150.0,
},
confidence: 0.92,
landmarks: None,
attributes: Some(result_attributes),
embedding: None,
};
Ok(FaceAnalysisResult::success(detected_face, processing_time))
}
// ========================================================================
// Helper Methods for Provider Implementations
// ========================================================================
async fn get_image_bytes(&self, source: &ImageSource) -> Result<Vec<u8>, FaceApiError> {
match source {
ImageSource::Variable(var) => {
Err(FaceApiError::InvalidInput(format!("Variable image source '{}' not supported in this context", var)))
}
ImageSource::Url(url) => {
let client = reqwest::Client::new();
let response = client
.get(url)
.send()
.await
.map_err(|e| FaceApiError::NetworkError(e.to_string()))?;
let bytes = response
.bytes()
.await
.map_err(|e| FaceApiError::NetworkError(e.to_string()))?;
Ok(bytes.to_vec())
}
ImageSource::Base64(data) => {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(data)
.map_err(|e| FaceApiError::ParseError(e.to_string()))
}
ImageSource::Bytes(bytes) | ImageSource::Binary(bytes) => Ok(bytes.clone()),
ImageSource::FilePath(path) => {
std::fs::read(path).map_err(|e| FaceApiError::InvalidInput(e.to_string()))
}
}
}
async fn simulate_face_detection(
&self,
image_bytes: &[u8],
options: &DetectionOptions,
) -> Vec<DetectedFace> {
// Simulate detection based on image size/content
// In production, actual detection algorithms would be used
let num_faces = if image_bytes.len() > 100_000 {
(image_bytes.len() / 500_000).min(5).max(1)
} else {
1
};
let max_faces = options.max_faces.unwrap_or(10);
let num_faces = num_faces.min(max_faces);
(0..num_faces)
.map(|i| {
let face_id = Uuid::new_v4();
DetectedFace {
id: face_id,
bounding_box: BoundingBox {
left: 100.0 + (i as f32 * 150.0),
top: 80.0 + (i as f32 * 20.0),
width: 120.0,
height: 150.0,
},
confidence: 0.95 - (i as f64 * 0.05),
landmarks: if options.return_landmarks.unwrap_or(false) {
Some(FaceLandmarks {
left_eye: Point2D { x: 140.0, y: 120.0 },
right_eye: Point2D { x: 180.0, y: 120.0 },
nose_tip: Point2D { x: 160.0, y: 150.0 },
mouth_left: Point2D { x: 145.0, y: 175.0 },
mouth_right: Point2D { x: 175.0, y: 175.0 },
left_eyebrow_left: None,
left_eyebrow_right: None,
right_eyebrow_left: None,
right_eyebrow_right: None,
})
} else {
None
},
attributes: if options.return_attributes.unwrap_or(false) {
Some(FaceAttributes {
age: Some(25.0 + (face_id.as_u128() % 40) as f32),
gender: Some(if face_id.as_u128() % 2 == 0 {
Gender::Male
} else {
Gender::Female
}),
emotion: None,
smile: Some(0.5),
glasses: Some(GlassesType::NoGlasses),
facial_hair: None,
head_pose: None,
blur: None,
exposure: None,
noise: None,
occlusion: None,
})
} else {
None
},
embedding: None,
}
})
.collect()
}
fn _generate_landmarks(&self) -> HashMap<String, (f32, f32)> {
let mut landmarks = HashMap::new();
landmarks.insert("left_eye".to_string(), (140.0, 120.0));
landmarks.insert("right_eye".to_string(), (180.0, 120.0));
landmarks.insert("nose_tip".to_string(), (160.0, 150.0));
landmarks.insert("mouth_left".to_string(), (145.0, 175.0));
landmarks.insert("mouth_right".to_string(), (175.0, 175.0));
landmarks
}
// ========================================================================
// Helper Methods
// ========================================================================
async fn get_or_detect_face_id(&self, source: &FaceSource) -> Result<Uuid, FaceApiError> {
match source {
FaceSource::FaceId(id) => Ok(*id),
FaceSource::DetectedFace(face) => Ok(face.id),
FaceSource::Image(image) => {
let result = self.detect_faces(image, &DetectionOptions::default()).await?;
result.faces.first()
.map(|f| f.id)
.ok_or(FaceApiError::NoFaceFound)
}
FaceSource::Embedding(_) => {
Err(FaceApiError::InvalidInput("Cannot get face ID from embedding".to_string()))
}
}
}
}