use crate::quality::NormalizedImage;
use crate::vision::{
ExtractionQualityReport, ExtractionRequest, OverlayArtifactSummary, RegionExtractor,
RegionKind, RegionObservation, RegionSet, RegionSource, RegionStatus, VisionError,
VisionReadinessReport, validate_mask,
};
use async_trait::async_trait;
use image::{ImageBuffer, Rgb};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::fmt;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus, Stdio};
use std::time::{Duration, Instant};
use tempfile::TempDir;
const HELPER_PYTHON_ENV: &str = "CHROMAFRAME_VISION_HELPER_PYTHON";
const HELPER_PACKAGE_ROOT_ENV: &str = "CHROMAFRAME_VISION_HELPER_PACKAGE_ROOT";
const MEDIAPIPE_TASK_ENV: &str = "CHROMAFRAME_MEDIAPIPE_TASK_PATH";
const FACE_PARSING_ONNX_ENV: &str = "CHROMAFRAME_FACE_PARSING_ONNX_PATH";
const ASSET_MANIFEST_ENV: &str = "CHROMAFRAME_VISION_ASSET_MANIFEST";
const HELPER_TIMEOUT_ENV: &str = "CHROMAFRAME_VISION_HELPER_TIMEOUT_MS";
const HELPER_SCHEMA_VERSION: u32 = 1;
const FACE_PARSER_MANIFEST_SCHEMA_VERSION: u32 = 1;
const FACE_PARSER_MODEL_ID: &str = "PayamFard123/dermaintel-face-parsing/resnet18.onnx";
const FACE_PARSER_SOURCE_REVISION: &str = "c1c5fd399cc72b45e3613019879fc6c17a7d8f6a";
const FACE_PARSER_EXPECTED_SHA256: &str =
"0d9bd318e46987c3bdbfacae9e2c0f461cae1c6ac6ea6d43bbe541a91727e33f";
const FACE_PARSER_SOURCE_URL: &str = "https://huggingface.co/PayamFard123/dermaintel-face-parsing/resolve/c1c5fd399cc72b45e3613019879fc6c17a7d8f6a/resnet18.onnx";
const FACE_PARSER_FALLBACK_SOURCE: &str =
"https://github.com/yakhyo/face-parsing/releases/download/weights/resnet18.onnx";
const FACE_PARSER_LICENSE: &str =
"MIT model card; weights licensed separately from ChromaFrame code";
const FACE_PARSER_LICENSE_ID: &str = "MIT";
const FACE_PARSER_PROVENANCE: &str =
"PayamFard123/dermaintel-face-parsing resnet18.onnx; yakhyo/face-parsing weights";
const FACE_PARSER_INPUT_WIDTH: u32 = 512;
const FACE_PARSER_INPUT_HEIGHT: u32 = 512;
const FACE_PARSER_MEAN: [f32; 3] = [0.485, 0.456, 0.406];
const FACE_PARSER_STD: [f32; 3] = [0.229, 0.224, 0.225];
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(20);
const READINESS_OUTPUT_LIMIT: u64 = 128;
const REQUIRED_PACKAGES: &[PythonPackage] = &[
PythonPackage {
display_name: "mediapipe",
import_name: "mediapipe",
},
PythonPackage {
display_name: "opencv-python-headless",
import_name: "cv2",
},
PythonPackage {
display_name: "numpy",
import_name: "numpy",
},
PythonPackage {
display_name: "Pillow",
import_name: "PIL",
},
];
const ONNX_RUNTIME_PACKAGE: PythonPackage = PythonPackage {
display_name: "onnxruntime",
import_name: "onnxruntime",
};
struct PythonPackage {
display_name: &'static str,
import_name: &'static str,
}
#[derive(Clone)]
pub struct HelperBackendConfig {
pub python_path: PathBuf,
pub mediapipe_task_path: Option<PathBuf>,
pub face_parsing_onnx_path: Option<PathBuf>,
pub asset_manifest_path: Option<PathBuf>,
pub timeout: Duration,
}
impl fmt::Debug for HelperBackendConfig {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("HelperBackendConfig")
.field("python_path", &redacted_path_state(&self.python_path))
.field(
"mediapipe_task_path",
&redacted_optional_path_state(&self.mediapipe_task_path),
)
.field(
"face_parsing_onnx_path",
&redacted_optional_path_state(&self.face_parsing_onnx_path),
)
.field(
"asset_manifest_path",
&redacted_optional_path_state(&self.asset_manifest_path),
)
.field("timeout_ms", &self.timeout.as_millis())
.finish()
}
}
impl HelperBackendConfig {
pub fn from_env() -> Self {
Self {
python_path: env_path(HELPER_PYTHON_ENV).unwrap_or_else(|| PathBuf::from("python3")),
mediapipe_task_path: env_path(MEDIAPIPE_TASK_ENV),
face_parsing_onnx_path: env_path(FACE_PARSING_ONNX_ENV),
asset_manifest_path: env_path(ASSET_MANIFEST_ENV),
timeout: env_timeout().unwrap_or(DEFAULT_TIMEOUT),
}
}
pub fn readiness_report(&self) -> VisionReadinessReport {
let python_version = sanitized_python_version(&self.python_path, self.timeout);
let missing_packages = missing_python_packages(
&self.python_path,
python_version.as_ref(),
self.timeout,
true,
);
let missing_models = missing_model_assets(self);
let version_supported = python_version
.as_deref()
.is_some_and(is_supported_python_version);
let backend_available =
version_supported && missing_models.is_empty() && missing_packages.is_empty();
VisionReadinessReport {
backend_available,
python_version,
missing_packages,
missing_models,
warnings: helper_warnings(backend_available, version_supported),
}
}
}
pub struct HelperRegionExtractor {
config: HelperBackendConfig,
}
impl HelperRegionExtractor {
pub fn from_env() -> Self {
Self {
config: HelperBackendConfig::from_env(),
}
}
pub fn new(config: HelperBackendConfig) -> Self {
Self { config }
}
pub fn readiness_report(&self) -> VisionReadinessReport {
self.config.readiness_report()
}
}
impl fmt::Debug for HelperRegionExtractor {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("HelperRegionExtractor")
.field("config", &self.config)
.finish()
}
}
#[async_trait]
impl RegionExtractor for HelperRegionExtractor {
async fn extract_regions(
&self,
image: &NormalizedImage,
request: &ExtractionRequest,
) -> Result<RegionSet, VisionError> {
let readiness = self.readiness_report();
if !readiness.backend_available {
return Err(first_readiness_error(readiness));
}
invoke_helper(&self.config, image, request)
}
}
#[derive(Clone, Serialize, Deserialize)]
struct HelperRequestEnvelope {
schema_version: u32,
request_id: String,
image_path: PathBuf,
image_width: u32,
image_height: u32,
requested_regions: Vec<RegionKind>,
min_region_pixels: usize,
erode_pixels: u32,
max_faces: u32,
mediapipe_task_path: PathBuf,
face_parsing_onnx_path: Option<PathBuf>,
asset_manifest_path: Option<PathBuf>,
overlay_output_path: Option<PathBuf>,
overlay_include_points: bool,
overlay_include_labels: bool,
}
impl fmt::Debug for HelperRequestEnvelope {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("HelperRequestEnvelope")
.field("schema_version", &self.schema_version)
.field("request_id", &self.request_id)
.field("image_path", &redacted_path_state(&self.image_path))
.field("image_dimensions", &(self.image_width, self.image_height))
.field("requested_region_count", &self.requested_regions.len())
.field("min_region_pixels", &self.min_region_pixels)
.field("erode_pixels", &self.erode_pixels)
.field("max_faces", &self.max_faces)
.field(
"mediapipe_task_path",
&redacted_path_state(&self.mediapipe_task_path),
)
.field(
"face_parsing_onnx_path",
&redacted_optional_path_state(&self.face_parsing_onnx_path),
)
.field(
"asset_manifest_path",
&redacted_optional_path_state(&self.asset_manifest_path),
)
.field(
"overlay_output_path",
&redacted_optional_path_state(&self.overlay_output_path),
)
.field("overlay_include_points", &self.overlay_include_points)
.field("overlay_include_labels", &self.overlay_include_labels)
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct HelperResponseEnvelope {
schema_version: u32,
request_id: String,
status: HelperStatus,
region_set: Option<RegionSet>,
overlay: Option<OverlayArtifactSummary>,
error_code: Option<HelperErrorCode>,
warnings: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum HelperStatus {
Ok,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum HelperErrorCode {
NoFace,
MultipleFaces,
ModelMissing,
PackageMissing,
BackendVersionMismatch,
ExtractionFailed,
InvalidRequest,
}
struct HelperProcessResult {
exited_successfully: bool,
diagnostics: RedactedProcessDiagnostics,
}
struct RedactedProcessDiagnostics {
stdout_bytes: usize,
stderr_bytes: usize,
}
impl RedactedProcessDiagnostics {
fn empty() -> Self {
Self {
stdout_bytes: 0,
stderr_bytes: 0,
}
}
fn public_code(&self) -> &'static str {
match (self.stdout_bytes > 0, self.stderr_bytes > 0) {
(true, true) => "stdout_stderr_redacted",
(true, false) => "stdout_redacted",
(false, true) => "stderr_redacted",
(false, false) => "no_response",
}
}
}
pub fn unavailable_region_set(width: u32, height: u32, reason: impl Into<String>) -> RegionSet {
let reason = sanitize_reason(reason.into());
RegionSet {
image_width: width,
image_height: height,
regions: Vec::new(),
extraction_quality: ExtractionQualityReport {
faces_detected: 0,
selected_face_index: None,
backend: "local_helper".to_string(),
warnings: vec![reason.clone()],
},
warnings: vec![reason],
}
}
fn invoke_helper(
config: &HelperBackendConfig,
image: &NormalizedImage,
request: &ExtractionRequest,
) -> Result<RegionSet, VisionError> {
let temp_dir = tempfile::Builder::new()
.prefix("chromaframe-vision-")
.tempdir()
.map_err(|_| VisionError::HelperIo)?;
let request_id = helper_request_id(image, request);
let image_path = temp_dir.path().join("input.png");
let request_path = temp_dir.path().join("request.json");
let response_path = temp_dir.path().join("response.json");
write_normalized_image(&image_path, image)?;
write_helper_request(
config,
image,
request,
&request_id,
&image_path,
&request_path,
)?;
let process = run_helper_process(config, &request_path, &response_path)?;
let response = match read_helper_response(&response_path) {
Ok(response) => response,
Err(error) if process.exited_successfully => return Err(error),
Err(_) => {
return Err(VisionError::HelperProtocolError(format!(
"helper_process_failed_{}",
process.diagnostics.public_code()
)));
}
};
if !process.exited_successfully && response.status == HelperStatus::Ok {
return Err(VisionError::HelperProtocolError(format!(
"helper_process_failed_{}",
process.diagnostics.public_code()
)));
}
let region_set =
validate_helper_response(response, &request_id, image.width, image.height, request)?;
cleanup_temp_dir(temp_dir, request.keep_debug_artifacts)?;
Ok(region_set)
}
fn write_helper_request(
config: &HelperBackendConfig,
image: &NormalizedImage,
request: &ExtractionRequest,
request_id: &str,
image_path: &Path,
request_path: &Path,
) -> Result<(), VisionError> {
let validated_overlay_output_path = validate_overlay_output_policy(request)?;
let Some(mediapipe_task_path) = &config.mediapipe_task_path else {
return Err(VisionError::ModelMissing(
"mediapipe_face_landmarker_task".to_string(),
));
};
let envelope = HelperRequestEnvelope {
schema_version: HELPER_SCHEMA_VERSION,
request_id: request_id.to_string(),
image_path: image_path.to_path_buf(),
image_width: image.width,
image_height: image.height,
requested_regions: request.requested_regions.clone(),
min_region_pixels: request.min_region_pixels,
erode_pixels: request.erode_pixels,
max_faces: request.max_faces,
mediapipe_task_path: mediapipe_task_path.clone(),
face_parsing_onnx_path: config.face_parsing_onnx_path.clone(),
asset_manifest_path: config.asset_manifest_path.clone(),
overlay_output_path: validated_overlay_output_path,
overlay_include_points: request
.overlay_request
.as_ref()
.is_some_and(|overlay| overlay.include_points),
overlay_include_labels: request
.overlay_request
.as_ref()
.is_some_and(|overlay| overlay.include_labels),
};
let json = serde_json::to_vec(&envelope)
.map_err(|_| VisionError::HelperProtocolError("request_json_encode_failed".to_string()))?;
fs::write(request_path, json).map_err(|_| VisionError::HelperIo)
}
fn validate_overlay_output_policy(
request: &ExtractionRequest,
) -> Result<Option<PathBuf>, VisionError> {
let Some(overlay) = &request.overlay_request else {
return Ok(None);
};
if !overlay.output_path.is_absolute() {
return Err(VisionError::HelperProtocolError(
"overlay_output_must_be_absolute".to_string(),
));
}
let Ok(repo_root) =
workspace_root().and_then(|root| root.canonicalize().map_err(|_| VisionError::HelperIo))
else {
return Ok(Some(overlay.output_path.clone()));
};
if !request.keep_debug_artifacts {
validate_overlay_target_outside_repo(&overlay.output_path, &repo_root)?;
validate_overlay_target_outside_repo(
&overlay_sidecar_path(&overlay.output_path),
&repo_root,
)?;
}
Ok(Some(overlay.output_path.clone()))
}
fn overlay_sidecar_path(path: &Path) -> PathBuf {
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.map(|extension| format!("{extension}.json"))
.unwrap_or_else(|| "json".to_string());
path.with_extension(extension)
}
fn validate_overlay_target_outside_repo(
target: &Path,
repo_root: &Path,
) -> Result<(), VisionError> {
if target.exists() {
let metadata = fs::symlink_metadata(target).map_err(|_| VisionError::HelperIo)?;
if metadata.file_type().is_symlink() {
return Err(VisionError::HelperProtocolError(
"overlay_existing_symlink_requires_debug_override".to_string(),
));
}
let resolved_target = target.canonicalize().map_err(|_| VisionError::HelperIo)?;
if resolved_target.starts_with(repo_root) {
return Err(VisionError::HelperProtocolError(
"overlay_output_inside_repo_requires_debug_override".to_string(),
));
}
return Ok(());
}
let resolved_parent = resolve_overlay_parent(target)?;
if resolved_parent.starts_with(repo_root) {
return Err(VisionError::HelperProtocolError(
"overlay_output_inside_repo_requires_debug_override".to_string(),
));
}
Ok(())
}
fn workspace_root() -> Result<PathBuf, VisionError> {
let mut current = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
loop {
if current.join(".git").exists() || is_workspace_manifest(¤t.join("Cargo.toml")) {
return Ok(current);
}
if !current.pop() {
return PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.canonicalize()
.map_err(|_| VisionError::HelperIo);
}
}
}
fn is_workspace_manifest(path: &Path) -> bool {
fs::read_to_string(path)
.map(|contents| contents.lines().any(|line| line.trim() == "[workspace]"))
.unwrap_or(false)
}
fn resolve_overlay_parent(candidate: &Path) -> Result<PathBuf, VisionError> {
let parent = candidate.parent().unwrap_or_else(|| Path::new("."));
let Some(existing) = nearest_existing_ancestor(parent) else {
return Err(VisionError::HelperProtocolError(
"overlay_parent_missing".to_string(),
));
};
existing.canonicalize().map_err(|_| VisionError::HelperIo)
}
fn nearest_existing_ancestor(path: &Path) -> Option<PathBuf> {
let mut current = path;
loop {
if current.exists() {
return Some(current.to_path_buf());
}
current = current.parent()?;
}
}
fn run_helper_process(
config: &HelperBackendConfig,
request_path: &Path,
response_path: &Path,
) -> Result<HelperProcessResult, VisionError> {
let mut child = Command::new(&config.python_path)
.arg("-m")
.arg("chromaframe_vision_helper")
.arg("extract")
.arg("--request-json")
.arg(request_path)
.arg("--response-json")
.arg(response_path)
.current_dir(helper_package_root())
.env("PYTHONPATH", helper_python_path())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|_| VisionError::BackendUnavailable)?;
let started_at = Instant::now();
loop {
if let Some(status) = child.try_wait().map_err(|_| VisionError::HelperIo)? {
return Ok(HelperProcessResult {
exited_successfully: status.success(),
diagnostics: RedactedProcessDiagnostics::empty(),
});
}
if started_at.elapsed() >= config.timeout {
let _ = child.kill();
let _ = child.wait();
return Err(VisionError::HelperTimeout);
}
std::thread::sleep(Duration::from_millis(20));
}
}
fn read_helper_response(response_path: &Path) -> Result<HelperResponseEnvelope, VisionError> {
let bytes = fs::read(response_path)
.map_err(|_| VisionError::HelperProtocolError("response_missing".to_string()))?;
serde_json::from_slice(&bytes)
.map_err(|_| VisionError::HelperProtocolError("response_json_invalid".to_string()))
}
fn validate_helper_response(
response: HelperResponseEnvelope,
request_id: &str,
width: u32,
height: u32,
request: &ExtractionRequest,
) -> Result<RegionSet, VisionError> {
if response.schema_version != HELPER_SCHEMA_VERSION {
return Err(VisionError::HelperProtocolError(
"schema_version_mismatch".to_string(),
));
}
if response.request_id != request_id {
return Err(VisionError::HelperProtocolError(
"request_id_mismatch".to_string(),
));
}
validate_response_status_consistency(&response)?;
if response.status != HelperStatus::Ok {
return Err(helper_error_to_vision_error(response.error_code));
}
let Some(region_set) = response.region_set else {
return Err(VisionError::HelperProtocolError(
"region_set_missing".to_string(),
));
};
validate_region_set_from_helper(®ion_set, width, height, request)?;
validate_overlay_response(&response.overlay, request)?;
region_set.parse()
}
fn validate_response_status_consistency(
response: &HelperResponseEnvelope,
) -> Result<(), VisionError> {
match (
response.status,
response.error_code,
response.region_set.is_some(),
) {
(HelperStatus::Ok, None, true) => Ok(()),
(HelperStatus::Error, Some(_), false) => Ok(()),
(HelperStatus::Ok, Some(_), _) => Err(VisionError::HelperProtocolError(
"ok_response_has_error_code".to_string(),
)),
(HelperStatus::Error, None, _) => Err(VisionError::HelperProtocolError(
"error_response_missing_error_code".to_string(),
)),
(HelperStatus::Error, Some(_), true) => Err(VisionError::HelperProtocolError(
"error_response_has_region_set".to_string(),
)),
(HelperStatus::Ok, None, false) => Err(VisionError::HelperProtocolError(
"ok_response_missing_region_set".to_string(),
)),
}
}
fn validate_region_set_from_helper(
region_set: &RegionSet,
width: u32,
height: u32,
request: &ExtractionRequest,
) -> Result<(), VisionError> {
if region_set.image_width != width || region_set.image_height != height {
return Err(VisionError::HelperProtocolError(
"dimension_mismatch".to_string(),
));
}
if region_set.extraction_quality.faces_detected == 0 {
return Err(VisionError::HelperProtocolError(
"face_count_missing".to_string(),
));
}
if region_set.extraction_quality.faces_detected > request.max_faces {
return Err(helper_error_to_vision_error(Some(
HelperErrorCode::MultipleFaces,
)));
}
let Some(selected_face_index) = region_set.extraction_quality.selected_face_index else {
return Err(VisionError::HelperProtocolError(
"selected_face_index_missing".to_string(),
));
};
if selected_face_index >= region_set.extraction_quality.faces_detected {
return Err(VisionError::HelperProtocolError(
"selected_face_index_out_of_range".to_string(),
));
}
for region in ®ion_set.regions {
validate_observation_from_helper(region, width, height, request.min_region_pixels)?;
}
for required in required_mvp_regions(request) {
if !has_measured_region(region_set, required) {
return Err(VisionError::HelperProtocolError(format!(
"mvp_region_missing_{required:?}"
)));
}
}
validate_requested_mvp_optional_regions(region_set, request)?;
Ok(())
}
fn validate_requested_mvp_optional_regions(
region_set: &RegionSet,
request: &ExtractionRequest,
) -> Result<(), VisionError> {
if request.requested_regions.contains(&RegionKind::Hair) {
let mut hair_observations = region_set
.regions
.iter()
.filter(|region| region.kind == RegionKind::Hair);
let Some(hair) = hair_observations.next() else {
return Err(VisionError::HelperProtocolError(
"mvp_region_missing_Hair".to_string(),
));
};
if hair_observations.next().is_some() {
return Err(VisionError::HelperProtocolError(
"semantic_parser_hair_duplicate".to_string(),
));
}
validate_requested_hair_observation(hair)?;
}
if request.requested_regions.contains(&RegionKind::Beard)
&& !has_region_observation(region_set, RegionKind::Beard)
{
return Err(VisionError::HelperProtocolError(
"mvp_region_missing_Beard".to_string(),
));
}
Ok(())
}
fn validate_requested_hair_observation(region: &RegionObservation) -> Result<(), VisionError> {
const EXPECTED_LOW_EVIDENCE_REASON: &str = "semantic_parser_hair_evidence_low";
if matches!(region.status, RegionStatus::Measured)
&& region.source == RegionSource::ParserMask
&& region.not_measured_reason.is_none()
{
return Ok(());
}
if let RegionStatus::NotMeasured { reason } = ®ion.status {
let duplicated_reason = region.not_measured_reason.as_deref();
if reason == EXPECTED_LOW_EVIDENCE_REASON
&& duplicated_reason == Some(EXPECTED_LOW_EVIDENCE_REASON)
{
return Ok(());
}
}
Err(VisionError::HelperProtocolError(
"semantic_parser_hair_response_invalid".to_string(),
))
}
fn validate_observation_from_helper(
region: &RegionObservation,
width: u32,
height: u32,
min_region_pixels: usize,
) -> Result<(), VisionError> {
match (®ion.status, region.source, ®ion.mask) {
(
RegionStatus::Measured,
RegionSource::MediapipeLandmarks | RegionSource::ParserMask,
Some(mask),
) => {
validate_mask(mask, width, height)?;
validate_region_area(region, mask, min_region_pixels)
}
(RegionStatus::Approximate, RegionSource::Approximation, Some(mask)) => {
if region
.approximate_reason
.as_deref()
.unwrap_or_default()
.is_empty()
{
return Err(VisionError::HelperProtocolError(
"approximate_reason_missing".to_string(),
));
}
validate_mask(mask, width, height)?;
validate_region_area(region, mask, min_region_pixels)
}
(RegionStatus::LowEvidence, RegionSource::Approximation, Some(mask)) => {
let reason = region
.approximate_reason
.as_deref()
.or(region.not_measured_reason.as_deref())
.unwrap_or_default();
if reason.is_empty() {
return Err(VisionError::HelperProtocolError(
"low_evidence_reason_missing".to_string(),
));
}
validate_mask(mask, width, height)?;
validate_region_area(region, mask, min_region_pixels)
}
(RegionStatus::NotMeasured { reason }, RegionSource::NotMeasured, None) => {
if reason.is_empty() {
return Err(VisionError::HelperProtocolError(
"not_measured_reason_missing".to_string(),
));
}
Ok(())
}
_ => Err(VisionError::HelperProtocolError(
"region_status_source_mismatch".to_string(),
)),
}
}
fn validate_region_area(
region: &RegionObservation,
mask: &crate::vision::RegionMask,
min_region_pixels: usize,
) -> Result<(), VisionError> {
let area = mask_area_estimate(mask);
if area >= min_region_pixels as f32 {
return Ok(());
}
Err(VisionError::HelperProtocolError(format!(
"region_area_too_small_{:?}",
region.kind
)))
}
fn mask_area_estimate(mask: &crate::vision::RegionMask) -> f32 {
match mask {
crate::vision::RegionMask::Polygon { polygons } => {
polygons.iter().map(|polygon| polygon_area(polygon)).sum()
}
crate::vision::RegionMask::RleBitmap { counts, .. } => counts
.iter()
.enumerate()
.filter(|(index, _)| index % 2 == 1)
.map(|(_, count)| *count as f32)
.sum(),
}
}
fn polygon_area(polygon: &[crate::vision::Point]) -> f32 {
if polygon.len() < 3 {
return 0.0;
}
let mut area = 0.0;
let mut previous = polygon.len() - 1;
for current in 0..polygon.len() {
area += polygon[previous].x * polygon[current].y - polygon[current].x * polygon[previous].y;
previous = current;
}
area.abs() * 0.5
}
fn validate_overlay_response(
overlay: &Option<OverlayArtifactSummary>,
request: &ExtractionRequest,
) -> Result<(), VisionError> {
if request.overlay_request.is_none() && overlay.is_some() {
return Err(VisionError::HelperProtocolError(
"unexpected_overlay_summary".to_string(),
));
}
if request.overlay_request.is_some() && overlay.is_none() {
return Err(VisionError::HelperProtocolError(
"overlay_summary_missing".to_string(),
));
}
Ok(())
}
fn required_mvp_regions(request: &ExtractionRequest) -> impl Iterator<Item = RegionKind> + '_ {
[
RegionKind::Skin,
RegionKind::Brow,
RegionKind::Iris,
RegionKind::Sclera,
RegionKind::Lip,
]
.into_iter()
.filter(|kind| request.requested_regions.contains(kind))
}
fn has_measured_region(region_set: &RegionSet, kind: RegionKind) -> bool {
region_set
.regions
.iter()
.any(|region| region.kind == kind && matches!(region.status, RegionStatus::Measured))
}
fn has_region_observation(region_set: &RegionSet, kind: RegionKind) -> bool {
region_set.regions.iter().any(|region| region.kind == kind)
}
fn helper_error_to_vision_error(error_code: Option<HelperErrorCode>) -> VisionError {
match error_code.unwrap_or(HelperErrorCode::ExtractionFailed) {
HelperErrorCode::ModelMissing => {
VisionError::ModelMissing("mediapipe_face_landmarker_task".to_string())
}
HelperErrorCode::PackageMissing => {
VisionError::PackageMissing("python_dependency".to_string())
}
HelperErrorCode::BackendVersionMismatch => VisionError::BackendVersionMismatch,
HelperErrorCode::NoFace => VisionError::HelperProtocolError("no_face_detected".to_string()),
HelperErrorCode::MultipleFaces => {
VisionError::HelperProtocolError("multiple_faces_detected".to_string())
}
HelperErrorCode::InvalidRequest => {
VisionError::HelperProtocolError("invalid_helper_request".to_string())
}
HelperErrorCode::ExtractionFailed => {
VisionError::HelperProtocolError("extraction_failed".to_string())
}
}
}
fn write_normalized_image(path: &Path, image: &NormalizedImage) -> Result<(), VisionError> {
let mut output = ImageBuffer::<Rgb<u8>, Vec<u8>>::new(image.width, image.height);
for (index, pixel) in image.pixels.iter().enumerate() {
let x = (index as u32) % image.width;
let y = (index as u32) / image.width;
output.put_pixel(x, y, Rgb([to_u8(pixel.r), to_u8(pixel.g), to_u8(pixel.b)]));
}
output.save(path).map_err(|_| VisionError::HelperIo)
}
fn to_u8(value: f32) -> u8 {
(value.clamp(0.0, 1.0) * 255.0).round() as u8
}
fn cleanup_temp_dir(temp_dir: TempDir, keep_debug_artifacts: bool) -> Result<(), VisionError> {
if keep_debug_artifacts {
let _ = temp_dir.keep();
return Ok(());
}
temp_dir.close().map_err(|_| VisionError::HelperIo)
}
fn helper_request_id(image: &NormalizedImage, request: &ExtractionRequest) -> String {
format!(
"vision-v{HELPER_SCHEMA_VERSION}-{}x{}-{}",
image.width,
image.height,
request.requested_regions.len()
)
}
fn helper_package_root() -> PathBuf {
env_path(HELPER_PACKAGE_ROOT_ENV)
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("vision-helper"))
}
fn helper_python_path() -> OsString {
helper_package_root().into_os_string()
}
fn env_path(name: &str) -> Option<PathBuf> {
let value = env::var_os(name)?;
if value.is_empty() {
return None;
}
Some(PathBuf::from(value))
}
fn env_timeout() -> Option<Duration> {
let millis = env::var(HELPER_TIMEOUT_ENV).ok()?.parse::<u64>().ok()?;
Some(Duration::from_millis(millis.clamp(1_000, 300_000)))
}
struct BoundedCommandResult {
status: Option<ExitStatus>,
stdout: Vec<u8>,
stderr: Vec<u8>,
timed_out: bool,
}
fn sanitized_python_version(python_path: &Path, timeout: Duration) -> Option<String> {
let output = run_bounded_command(python_path, &["--version"], timeout, true).ok()?;
if output.timed_out || !output.status.is_some_and(|status| status.success()) {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(version) = parse_canonical_python_version(stdout.trim()) {
return Some(version);
}
let stderr = String::from_utf8_lossy(&output.stderr);
parse_canonical_python_version(stderr.trim())
}
fn parse_canonical_python_version(output: &str) -> Option<String> {
let trimmed = output.trim();
let mut parts = trimmed.split_whitespace();
let prefix = parts.next()?;
let version = parts.next()?;
if parts.next().is_some() || prefix != "Python" {
return None;
}
let mut numbers = version.split('.');
let major = numbers.next()?;
let minor = numbers.next()?;
let patch = numbers.next()?;
if numbers.next().is_some() || major != "3" || !matches!(minor, "11" | "12") {
return None;
}
if !patch.chars().all(|character| character.is_ascii_digit()) {
return None;
}
Some(format!("Python {major}.{minor}.{patch}"))
}
fn is_supported_python_version(version: &str) -> bool {
version.starts_with("Python 3.11.") || version.starts_with("Python 3.12.")
}
fn missing_python_packages(
python_path: &Path,
python_version: Option<&String>,
timeout: Duration,
parser_configured: bool,
) -> Vec<String> {
let required_packages = required_python_packages(parser_configured);
if python_version.is_none() {
return required_packages
.iter()
.map(|package| package.display_name.to_string())
.collect();
}
required_packages
.iter()
.filter(|package| !python_can_import(python_path, package.import_name, timeout))
.map(|package| package.display_name.to_string())
.collect()
}
fn required_python_packages(_parser_configured: bool) -> Vec<&'static PythonPackage> {
let mut packages: Vec<&'static PythonPackage> = REQUIRED_PACKAGES.iter().collect();
packages.push(&ONNX_RUNTIME_PACKAGE);
packages
}
fn python_can_import(python_path: &Path, import_name: &str, timeout: Duration) -> bool {
if !is_safe_import_name(import_name) {
return false;
}
run_bounded_command(
python_path,
&["-c", &format!("import {import_name}")],
timeout,
false,
)
.map(|output| !output.timed_out && output.status.is_some_and(|status| status.success()))
.unwrap_or(false)
}
fn run_bounded_command(
program: &Path,
args: &[&str],
timeout: Duration,
capture_stdout: bool,
) -> Result<BoundedCommandResult, VisionError> {
let stdout_capture = if capture_stdout {
Some(
tempfile::Builder::new()
.prefix("chromaframe-readiness-")
.tempfile()
.map_err(|_| VisionError::HelperIo)?,
)
} else {
None
};
let stdout = match &stdout_capture {
Some(file) => Stdio::from(
file.as_file()
.try_clone()
.map_err(|_| VisionError::HelperIo)?,
),
None => Stdio::null(),
};
let stderr_capture = if capture_stdout {
Some(
tempfile::Builder::new()
.prefix("chromaframe-readiness-")
.tempfile()
.map_err(|_| VisionError::HelperIo)?,
)
} else {
None
};
let stderr = match &stderr_capture {
Some(file) => Stdio::from(
file.as_file()
.try_clone()
.map_err(|_| VisionError::HelperIo)?,
),
None => Stdio::null(),
};
let mut child = Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(stdout)
.stderr(stderr)
.spawn()
.map_err(|_| VisionError::BackendUnavailable)?;
let started_at = Instant::now();
loop {
if let Some(status) = child.try_wait().map_err(|_| VisionError::HelperIo)? {
let stdout = read_captured_output(stdout_capture.as_ref())?;
let stderr = read_captured_output(stderr_capture.as_ref())?;
return Ok(BoundedCommandResult {
status: Some(status),
stdout,
stderr,
timed_out: false,
});
}
if started_at.elapsed() >= timeout {
let _ = child.kill();
let _ = child.wait();
return Ok(BoundedCommandResult {
status: None,
stdout: Vec::new(),
stderr: Vec::new(),
timed_out: true,
});
}
std::thread::sleep(Duration::from_millis(20));
}
}
fn read_captured_output(file: Option<&tempfile::NamedTempFile>) -> Result<Vec<u8>, VisionError> {
let Some(file) = file else {
return Ok(Vec::new());
};
let mut output = Vec::new();
let mut reader = fs::File::open(file.path()).map_err(|_| VisionError::HelperIo)?;
reader
.by_ref()
.take(READINESS_OUTPUT_LIMIT)
.read_to_end(&mut output)
.map_err(|_| VisionError::HelperIo)?;
Ok(output)
}
fn is_safe_import_name(import_name: &str) -> bool {
!import_name.is_empty()
&& import_name
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_')
}
fn missing_model_assets(config: &HelperBackendConfig) -> Vec<String> {
let mut missing = Vec::new();
push_missing_path(
&mut missing,
"mediapipe_face_landmarker_task",
config.mediapipe_task_path.as_deref(),
);
push_missing_path(
&mut missing,
"face_parsing_onnx",
config.face_parsing_onnx_path.as_deref(),
);
if let Some(path) = &config.face_parsing_onnx_path {
validate_optional_parser_manifest(
&mut missing,
path,
config.asset_manifest_path.as_deref(),
);
} else {
missing.push("face_parsing_manifest_not_configured".to_string());
}
missing
}
#[derive(Deserialize)]
struct ParserAssetManifest {
face_parsing_onnx: ParserAssetEntry,
}
#[derive(Deserialize)]
struct ParserAssetEntry {
manifest_schema_version: u32,
model_id: String,
sha256: String,
expected_sha256: String,
license: String,
#[serde(default)]
license_id: String,
approved_for_use: bool,
model_type: String,
source_url: String,
source_revision: String,
fallback_source: String,
provenance: String,
labels: BTreeMap<String, u32>,
input: ParserInputSpec,
normalization: ParserNormalizationSpec,
output: ParserOutputSpec,
}
#[derive(Deserialize)]
struct ParserInputSpec {
width: u32,
height: u32,
}
#[derive(Deserialize)]
struct ParserNormalizationSpec {
mean: [f32; 3],
std: [f32; 3],
}
#[derive(Deserialize)]
struct ParserOutputSpec {
#[serde(rename = "type")]
output_type: String,
}
fn validate_optional_parser_manifest(
missing: &mut Vec<String>,
parser_path: &Path,
manifest_path: Option<&Path>,
) {
let Some(manifest_path) = manifest_path else {
missing.push("face_parsing_manifest_not_configured".to_string());
return;
};
if !manifest_path.is_file() {
missing.push("face_parsing_manifest_not_readable".to_string());
return;
}
let Ok(bytes) = fs::read(manifest_path) else {
missing.push("face_parsing_manifest_not_readable".to_string());
return;
};
let Ok(manifest) = serde_json::from_slice::<ParserAssetManifest>(&bytes) else {
missing.push("face_parsing_manifest_invalid".to_string());
return;
};
if let Some(reason) = parser_manifest_rejection_reason(&manifest.face_parsing_onnx) {
missing.push(reason.to_string());
return;
}
let Ok(parser_bytes) = fs::read(parser_path) else {
missing.push("face_parsing_onnx_not_readable".to_string());
return;
};
let digest = Sha256::digest(parser_bytes);
let actual = encode_lower_hex(digest.as_ref());
if actual != manifest.face_parsing_onnx.sha256.to_ascii_lowercase() {
missing.push("face_parsing_sha256_mismatch".to_string());
}
}
fn parser_manifest_rejection_reason(entry: &ParserAssetEntry) -> Option<&'static str> {
const REJECTED_LABELS: &[&str] = &["beard", "moustache", "mustache", "stubble", "facial_hair"];
if !entry.approved_for_use || entry.model_type != "baseline_face_parser" {
return Some("face_parsing_license_not_approved");
}
if entry.manifest_schema_version != FACE_PARSER_MANIFEST_SCHEMA_VERSION
|| entry.model_id != FACE_PARSER_MODEL_ID
|| entry.source_revision != FACE_PARSER_SOURCE_REVISION
{
return Some("face_parsing_license_not_approved");
}
if entry.expected_sha256 != FACE_PARSER_EXPECTED_SHA256
|| entry.sha256 != FACE_PARSER_EXPECTED_SHA256
{
return Some("face_parsing_license_not_approved");
}
if entry.source_url != FACE_PARSER_SOURCE_URL
|| entry.fallback_source != FACE_PARSER_FALLBACK_SOURCE
|| entry.license != FACE_PARSER_LICENSE
|| entry.license_id != FACE_PARSER_LICENSE_ID
|| entry.provenance != FACE_PARSER_PROVENANCE
{
return Some("face_parsing_license_not_approved");
}
if entry.sha256.len() != 64
|| !entry
.sha256
.chars()
.all(|character| character.is_ascii_hexdigit())
{
return Some("face_parsing_license_not_approved");
}
if entry.input.width != FACE_PARSER_INPUT_WIDTH
|| entry.input.height != FACE_PARSER_INPUT_HEIGHT
{
return Some("face_parsing_license_not_approved");
}
if !float_triplet_matches(entry.normalization.mean, FACE_PARSER_MEAN)
|| !float_triplet_matches(entry.normalization.std, FACE_PARSER_STD)
{
return Some("face_parsing_license_not_approved");
}
if entry.output.output_type.as_str() != "logits_argmax" {
return Some("face_parsing_license_not_approved");
}
if entry.labels.is_empty() {
return Some("face_parsing_labels_invalid");
}
if entry
.labels
.keys()
.any(|label| REJECTED_LABELS.contains(&label.as_str()))
{
return Some("face_parsing_facial_hair_labels_rejected");
}
if !labels_match_expected_contract(&entry.labels) {
return Some("face_parsing_labels_invalid");
}
None
}
fn float_triplet_matches(actual: [f32; 3], expected: [f32; 3]) -> bool {
actual
.iter()
.zip(expected.iter())
.all(|(left, right)| left.is_finite() && (*left - *right).abs() <= 0.000_001)
}
fn labels_match_expected_contract(labels: &BTreeMap<String, u32>) -> bool {
const EXPECTED_LABELS: &[(&str, u32)] = &[
("background", 0),
("skin", 1),
("l_brow", 2),
("r_brow", 3),
("l_eye", 4),
("r_eye", 5),
("eye_g", 6),
("l_ear", 7),
("r_ear", 8),
("ear_r", 9),
("nose", 10),
("mouth", 11),
("u_lip", 12),
("l_lip", 13),
("neck", 14),
("neck_l", 15),
("cloth", 16),
("hair", 17),
("hat", 18),
];
labels.len() == EXPECTED_LABELS.len()
&& EXPECTED_LABELS
.iter()
.all(|(label, id)| labels.get(*label) == Some(id))
}
fn encode_lower_hex(bytes: &[u8]) -> String {
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
encoded.push(HEX_DIGITS[(byte >> 4) as usize] as char);
encoded.push(HEX_DIGITS[(byte & 0x0f) as usize] as char);
}
encoded
}
fn push_missing_path(missing: &mut Vec<String>, label: &str, path: Option<&Path>) {
let Some(path) = path else {
missing.push(format!("{label}_not_configured"));
return;
};
if !path.is_file() {
missing.push(format!("{label}_not_readable"));
}
}
fn helper_warnings(backend_available: bool, version_supported: bool) -> Vec<String> {
let mut warnings = Vec::new();
if !version_supported {
warnings.push("python_version_target_3_11_or_3_12".to_string());
}
if !backend_available {
warnings.push("local_helper_not_ready".to_string());
}
warnings
}
fn first_readiness_error(readiness: VisionReadinessReport) -> VisionError {
if readiness
.python_version
.as_deref()
.is_some_and(|version| !is_supported_python_version(version))
{
return VisionError::BackendVersionMismatch;
}
if let Some(package) = readiness.missing_packages.first() {
return VisionError::PackageMissing(package.clone());
}
if let Some(model) = readiness.missing_models.first() {
if model.contains("sha256_mismatch") {
return VisionError::ModelChecksumMismatch;
}
if model.contains("license_not_approved") {
return VisionError::UnsupportedModelLicense;
}
return VisionError::ModelMissing(model.clone());
}
VisionError::BackendUnavailable
}
fn sanitize_reason(value: String) -> String {
let safe: String = value
.chars()
.filter(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.' | ' '))
.take(96)
.collect();
if safe.trim().is_empty() {
"unspecified".to_string()
} else {
safe
}
}
fn redacted_path_state(path: &Path) -> &'static str {
if path.as_os_str().is_empty() {
"not_configured"
} else {
"configured_redacted"
}
}
fn redacted_optional_path_state(path: &Option<PathBuf>) -> &'static str {
if path.is_some() {
"configured_redacted"
} else {
"not_configured"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vision::{Point, RegionMask};
#[test]
fn rejects_mismatched_helper_request_id() {
let response = HelperResponseEnvelope {
schema_version: HELPER_SCHEMA_VERSION,
request_id: "wrong".to_string(),
status: HelperStatus::Ok,
region_set: None,
overlay: None,
error_code: None,
warnings: Vec::new(),
};
assert!(matches!(
validate_helper_response(response, "expected", 4, 4, &ExtractionRequest::default()),
Err(VisionError::HelperProtocolError(_))
));
}
#[test]
fn validates_mvp_region_presence() {
let square = RegionMask::Polygon {
polygons: vec![vec![
Point { x: 0.0, y: 0.0 },
Point { x: 9.0, y: 0.0 },
Point { x: 9.0, y: 9.0 },
Point { x: 0.0, y: 9.0 },
]],
};
let region = |kind| RegionObservation {
kind,
status: RegionStatus::Measured,
source: RegionSource::MediapipeLandmarks,
confidence: 0.9,
mask: Some(square.clone()),
sample_hint: Some(20),
approximate_reason: None,
not_measured_reason: None,
};
let region_set = RegionSet {
image_width: 10,
image_height: 10,
regions: vec![
region(RegionKind::Skin),
region(RegionKind::Brow),
region(RegionKind::Iris),
region(RegionKind::Sclera),
region(RegionKind::Lip),
],
extraction_quality: ExtractionQualityReport {
faces_detected: 1,
selected_face_index: Some(0),
backend: "mediapipe".to_string(),
warnings: Vec::new(),
},
warnings: Vec::new(),
};
let response = HelperResponseEnvelope {
schema_version: HELPER_SCHEMA_VERSION,
request_id: "ok".to_string(),
status: HelperStatus::Ok,
region_set: Some(region_set),
overlay: None,
error_code: None,
warnings: Vec::new(),
};
assert!(
validate_helper_response(
response,
"ok",
10,
10,
&ExtractionRequest {
requested_regions: vec![
RegionKind::Skin,
RegionKind::Brow,
RegionKind::Iris,
RegionKind::Sclera,
RegionKind::Lip,
],
..ExtractionRequest::default()
}
)
.is_ok()
);
}
#[test]
fn protocol_error_codes_are_preserved() {
for (code, expected) in [
(HelperErrorCode::NoFace, "no_face_detected"),
(HelperErrorCode::MultipleFaces, "multiple_faces_detected"),
(HelperErrorCode::InvalidRequest, "invalid_helper_request"),
] {
let response = HelperResponseEnvelope {
schema_version: HELPER_SCHEMA_VERSION,
request_id: "req".to_string(),
status: HelperStatus::Error,
region_set: None,
overlay: None,
error_code: Some(code),
warnings: Vec::new(),
};
let error =
validate_helper_response(response, "req", 4, 4, &ExtractionRequest::default())
.unwrap_err();
assert!(error.to_string().contains(expected));
}
let package_error = helper_error_to_vision_error(Some(HelperErrorCode::PackageMissing));
assert!(matches!(package_error, VisionError::PackageMissing(_)));
}
#[test]
fn rejects_inconsistent_protocol_responses() {
let mut response = error_response(HelperErrorCode::NoFace);
response.error_code = None;
assert!(matches!(
validate_helper_response(response, "req", 4, 4, &ExtractionRequest::default()),
Err(VisionError::HelperProtocolError(_))
));
let mut response = ok_response(valid_region_set(10, 10));
response.error_code = Some(HelperErrorCode::NoFace);
assert!(matches!(
validate_helper_response(response, "req", 10, 10, &ExtractionRequest::default()),
Err(VisionError::HelperProtocolError(_))
));
}
#[test]
fn validates_face_count_and_selected_index() {
let mut region_set = valid_region_set(10, 10);
region_set.extraction_quality.faces_detected = 2;
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(_))
));
let mut region_set = valid_region_set(10, 10);
region_set.extraction_quality.selected_face_index = None;
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(_))
));
let mut region_set = valid_region_set(10, 10);
region_set.extraction_quality.selected_face_index = Some(1);
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(_))
));
}
#[test]
fn rejects_tiny_measured_region_area() {
let tiny = RegionMask::Polygon {
polygons: vec![vec![
Point { x: 0.0, y: 0.0 },
Point { x: 1.0, y: 0.0 },
Point { x: 1.0, y: 1.0 },
]],
};
let region = RegionObservation {
kind: RegionKind::Skin,
status: RegionStatus::Measured,
source: RegionSource::MediapipeLandmarks,
confidence: 0.9,
mask: Some(tiny),
sample_hint: Some(1),
approximate_reason: None,
not_measured_reason: None,
};
assert!(matches!(
validate_observation_from_helper(®ion, 4, 4, 20),
Err(VisionError::HelperProtocolError(_))
));
let inflated_hint_region = RegionObservation {
sample_hint: Some(10_000),
..region
};
assert!(matches!(
validate_observation_from_helper(&inflated_hint_region, 4, 4, 20),
Err(VisionError::HelperProtocolError(_))
));
}
#[test]
fn rejects_tiny_rle_with_inflated_sample_hint() {
let region = RegionObservation {
kind: RegionKind::Skin,
status: RegionStatus::Measured,
source: RegionSource::MediapipeLandmarks,
confidence: 0.9,
mask: Some(RegionMask::RleBitmap {
width: 10,
height: 10,
counts: vec![99, 1],
}),
sample_hint: Some(10_000),
approximate_reason: None,
not_measured_reason: None,
};
assert!(matches!(
validate_observation_from_helper(®ion, 10, 10, 20),
Err(VisionError::HelperProtocolError(_))
));
}
#[test]
fn accepts_sufficient_area_without_sample_hint() {
let region = RegionObservation {
kind: RegionKind::Skin,
status: RegionStatus::Measured,
source: RegionSource::MediapipeLandmarks,
confidence: 0.9,
mask: Some(RegionMask::Polygon {
polygons: vec![vec![
Point { x: 0.0, y: 0.0 },
Point { x: 9.0, y: 0.0 },
Point { x: 9.0, y: 9.0 },
Point { x: 0.0, y: 9.0 },
]],
}),
sample_hint: None,
approximate_reason: None,
not_measured_reason: None,
};
assert!(validate_observation_from_helper(®ion, 10, 10, 20).is_ok());
}
#[test]
fn validates_overlay_opt_in_contract() {
let response = ok_response(valid_region_set(10, 10));
assert!(
validate_helper_response(response, "req", 10, 10, &ExtractionRequest::default())
.is_ok()
);
let mut response = ok_response(valid_region_set(10, 10));
response.overlay = Some(OverlayArtifactSummary {
output_path_redacted: "[REDACTED]".to_string(),
dimensions: (4, 4),
region_count: 5,
color_key: Vec::new(),
});
assert!(matches!(
validate_helper_response(response, "req", 10, 10, &ExtractionRequest::default()),
Err(VisionError::HelperProtocolError(_))
));
let request = ExtractionRequest {
overlay_request: Some(crate::vision::OverlayRequest {
output_path: "overlay.png".into(),
include_points: true,
include_labels: true,
}),
..ExtractionRequest::default()
};
let mut response = ok_response(valid_region_set(10, 10));
response.overlay = Some(OverlayArtifactSummary {
output_path_redacted: "[REDACTED]".to_string(),
dimensions: (4, 4),
region_count: 5,
color_key: Vec::new(),
});
assert!(validate_helper_response(response, "req", 10, 10, &request).is_ok());
}
#[test]
fn rejects_repo_overlay_path_without_debug_override() {
let workspace_overlay = workspace_root().unwrap().join("target/private-overlay.png");
let mut request = ExtractionRequest {
overlay_request: Some(crate::vision::OverlayRequest {
output_path: workspace_overlay,
include_points: false,
include_labels: false,
}),
..ExtractionRequest::default()
};
assert!(matches!(
validate_overlay_output_policy(&request),
Err(VisionError::HelperProtocolError(_))
));
request.keep_debug_artifacts = true;
assert!(validate_overlay_output_policy(&request).is_ok());
}
#[test]
fn rejects_relative_parent_overlay_path_before_serialization() {
let request = ExtractionRequest {
overlay_request: Some(crate::vision::OverlayRequest {
output_path: "../outside/overlay.png".into(),
include_points: false,
include_labels: false,
}),
..ExtractionRequest::default()
};
assert!(matches!(
validate_overlay_output_policy(&request),
Err(VisionError::HelperProtocolError(_))
));
}
#[test]
fn rejects_workspace_overlay_path_outside_crate_without_debug_override() {
let workspace_overlay = workspace_root().unwrap().join("workspace-overlay.png");
let mut request = ExtractionRequest {
overlay_request: Some(crate::vision::OverlayRequest {
output_path: workspace_overlay,
include_points: false,
include_labels: false,
}),
..ExtractionRequest::default()
};
assert!(matches!(
validate_overlay_output_policy(&request),
Err(VisionError::HelperProtocolError(_))
));
request.keep_debug_artifacts = true;
assert!(validate_overlay_output_policy(&request).is_ok());
}
#[test]
fn failed_process_errors_do_not_expose_raw_output_tokens() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
let response_path = temp.path().join("missing-response.json");
let request_path = temp.path().join("request.json");
fs::write(&request_path, b"{}").unwrap();
fs::write(
&script,
"#!/bin/sh\necho sk-test-secret-api-key-token\necho /Users/alice/private/image.png raw stdout content 1>&2\necho stderr-gemini-api-key-like-token 1>&2\nexit 1\n",
)
.unwrap();
make_executable(&script);
let config = HelperBackendConfig {
python_path: script,
mediapipe_task_path: None,
face_parsing_onnx_path: None,
asset_manifest_path: None,
timeout: DEFAULT_TIMEOUT,
};
let process = run_helper_process(&config, &request_path, &response_path).unwrap();
let error = match read_helper_response(&response_path) {
Ok(_) => unreachable!(),
Err(_) => VisionError::HelperProtocolError(format!(
"helper_process_failed_{}",
process.diagnostics.public_code()
)),
};
let display = error.to_string();
assert!(display.contains("no_response"));
assert!(!display.contains("sk-secret"));
assert!(!display.contains("sk-test"));
assert!(!display.contains("gemini"));
assert!(!display.contains("api-key"));
assert!(!display.contains("/Users"));
assert!(!display.contains("alice"));
assert!(!display.contains("image.png"));
assert!(!display.contains("person"));
}
#[test]
fn readiness_version_check_times_out() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
fs::write(&script, "#!/bin/sh\nsleep 2\n").unwrap();
make_executable(&script);
let started = Instant::now();
let config = HelperBackendConfig {
python_path: script,
mediapipe_task_path: None,
face_parsing_onnx_path: None,
asset_manifest_path: None,
timeout: Duration::from_millis(200),
};
let report = config.readiness_report();
assert!(started.elapsed() < Duration::from_secs(1));
assert!(report.python_version.is_none());
assert!(!report.backend_available);
}
#[test]
fn readiness_import_check_times_out() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
fs::write(
&script,
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'Python 3.12.0'; exit 0; fi\nsleep 2\n",
)
.unwrap();
make_executable(&script);
let started = Instant::now();
assert_eq!(
sanitized_python_version(&script, Duration::from_millis(1_000)).as_deref(),
Some("Python 3.12.0")
);
assert!(!python_can_import(
&script,
"mediapipe",
Duration::from_millis(200)
));
assert!(started.elapsed() < Duration::from_secs(2));
}
#[test]
fn readiness_version_accepts_stderr_banner() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
fs::write(
&script,
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'Python 3.12.0' 1>&2; exit 0; fi\nexit 1\n",
)
.unwrap();
make_executable(&script);
assert_eq!(
sanitized_python_version(&script, DEFAULT_TIMEOUT).as_deref(),
Some("Python 3.12.0")
);
}
#[test]
fn readiness_import_does_not_wait_for_descendant_holding_output() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
fs::write(
&script,
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'Python 3.12.0'; exit 0; fi\nexec python3 -c 'import subprocess, sys; subprocess.Popen([\"sleep\", \"3\"]); sys.exit(1)'\n",
)
.unwrap();
make_executable(&script);
let started = Instant::now();
assert!(!python_can_import(
&script,
"mediapipe",
Duration::from_millis(1_500)
));
assert!(started.elapsed() < Duration::from_secs(2));
}
#[test]
fn readiness_version_does_not_wait_for_descendant_holding_stdout() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
fs::write(
&script,
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then exec python3 -c 'import subprocess; subprocess.Popen([\"sleep\", \"3\"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, close_fds=True); print(\"Python 3.12.0\")'; fi\nexit 1\n",
)
.unwrap();
make_executable(&script);
let started = Instant::now();
assert_eq!(
sanitized_python_version(&script, Duration::from_millis(1_500)).as_deref(),
Some("Python 3.12.0")
);
assert!(started.elapsed() < Duration::from_secs(2));
}
#[test]
fn helper_process_does_not_wait_for_descendant_holding_output() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
let response_path = temp.path().join("response.json");
let request_path = temp.path().join("request.json");
fs::write(&request_path, b"{}").unwrap();
fs::write(
&script,
"#!/bin/sh\nexec python3 -c 'import subprocess, sys; subprocess.Popen([\"sleep\", \"3\"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, close_fds=True); sys.exit(1)'\n",
)
.unwrap();
make_executable(&script);
let config = HelperBackendConfig {
python_path: script,
mediapipe_task_path: None,
face_parsing_onnx_path: None,
asset_manifest_path: None,
timeout: Duration::from_millis(1_500),
};
let started = Instant::now();
let process = run_helper_process(&config, &request_path, &response_path).unwrap();
assert!(!process.exited_successfully);
assert!(started.elapsed() < Duration::from_secs(2));
}
#[test]
fn readiness_python_version_rejects_secret_or_path_tokens() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
fs::write(
&script,
"#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then echo 'Python 3.12.0 sk-test GEMINI_API_KEY /Users/alice/private/image.png'; exit 0; fi\nexit 1\n",
)
.unwrap();
make_executable(&script);
let config = HelperBackendConfig {
python_path: script,
mediapipe_task_path: None,
face_parsing_onnx_path: None,
asset_manifest_path: None,
timeout: Duration::from_millis(200),
};
let report = config.readiness_report();
let debug = format!("{report:?}");
assert!(report.python_version.is_none());
assert!(!debug.contains("sk-test"));
assert!(!debug.contains("GEMINI_API_KEY"));
assert!(!debug.contains("/Users"));
assert!(!debug.contains("image.png"));
}
#[test]
fn helper_request_debug_redacts_paths() {
let envelope = HelperRequestEnvelope {
schema_version: HELPER_SCHEMA_VERSION,
request_id: "req".to_string(),
image_path: "/Users/private/input.png".into(),
image_width: 10,
image_height: 10,
requested_regions: vec![RegionKind::Skin],
min_region_pixels: 20,
erode_pixels: 2,
max_faces: 1,
mediapipe_task_path: "/Users/private/model.task".into(),
face_parsing_onnx_path: Some("/Users/private/parser.onnx".into()),
asset_manifest_path: Some("/Users/private/manifest.json".into()),
overlay_output_path: Some("/Users/private/overlay.png".into()),
overlay_include_points: true,
overlay_include_labels: true,
};
let debug = format!("{envelope:?}");
assert!(debug.contains("configured_redacted"));
assert!(!debug.contains("/Users"));
assert!(!debug.contains("input.png"));
assert!(!debug.contains("model.task"));
}
#[cfg(unix)]
#[test]
fn rejects_symlinked_overlay_parent_into_repo_without_debug_override() {
let temp = tempfile::tempdir().unwrap();
let link = temp.path().join("repo-link");
std::os::unix::fs::symlink(env!("CARGO_MANIFEST_DIR"), &link).unwrap();
let request = ExtractionRequest {
overlay_request: Some(crate::vision::OverlayRequest {
output_path: link.join("symlink-overlay.png"),
include_points: false,
include_labels: false,
}),
..ExtractionRequest::default()
};
assert!(matches!(
validate_overlay_output_policy(&request),
Err(VisionError::HelperProtocolError(_))
));
}
#[cfg(unix)]
#[test]
fn rejects_existing_overlay_leaf_symlink_into_workspace() {
let temp = tempfile::tempdir().unwrap();
let overlay = temp.path().join("overlay.png");
std::os::unix::fs::symlink(workspace_root().unwrap().join("Cargo.toml"), &overlay).unwrap();
let request = ExtractionRequest {
overlay_request: Some(crate::vision::OverlayRequest {
output_path: overlay,
include_points: false,
include_labels: false,
}),
..ExtractionRequest::default()
};
assert!(matches!(
validate_overlay_output_policy(&request),
Err(VisionError::HelperProtocolError(_))
));
}
#[cfg(unix)]
#[test]
fn rejects_existing_sidecar_leaf_symlink_into_workspace() {
let temp = tempfile::tempdir().unwrap();
let overlay = temp.path().join("overlay.png");
let sidecar = overlay_sidecar_path(&overlay);
std::os::unix::fs::symlink(workspace_root().unwrap().join("Cargo.toml"), &sidecar).unwrap();
let request = ExtractionRequest {
overlay_request: Some(crate::vision::OverlayRequest {
output_path: overlay,
include_points: false,
include_labels: false,
}),
..ExtractionRequest::default()
};
assert!(matches!(
validate_overlay_output_policy(&request),
Err(VisionError::HelperProtocolError(_))
));
}
fn expected_label_json() -> &'static str {
r#"{"background":0,"skin":1,"l_brow":2,"r_brow":3,"l_eye":4,"r_eye":5,"eye_g":6,"l_ear":7,"r_ear":8,"ear_r":9,"nose":10,"mouth":11,"u_lip":12,"l_lip":13,"neck":14,"neck_l":15,"cloth":16,"hair":17,"hat":18}"#
}
fn parser_manifest_json(
license: &str,
approved_for_use: bool,
sha256: &str,
labels: &str,
) -> String {
format!(
r#"{{"face_parsing_onnx":{{"manifest_schema_version":1,"model_id":"{}","sha256":"{}","expected_sha256":"{}","license":"{}","license_id":"{}","approved_for_use":{},"model_type":"baseline_face_parser","source_url":"{}","source_revision":"{}","fallback_source":"{}","provenance":"{}","labels":{},"input":{{"width":512,"height":512}},"normalization":{{"mean":[0.485,0.456,0.406],"std":[0.229,0.224,0.225]}},"output":{{"type":"logits_argmax"}}}}}}"#,
FACE_PARSER_MODEL_ID,
sha256,
FACE_PARSER_EXPECTED_SHA256,
license,
FACE_PARSER_LICENSE_ID,
approved_for_use,
FACE_PARSER_SOURCE_URL,
FACE_PARSER_SOURCE_REVISION,
FACE_PARSER_FALLBACK_SOURCE,
FACE_PARSER_PROVENANCE,
labels
)
}
#[test]
fn manifest_license_and_checksum_map_to_typed_errors() {
let temp = tempfile::tempdir().unwrap();
let parser_path = temp.path().join("parser.onnx");
fs::write(&parser_path, b"parser").unwrap();
let manifest_path = temp.path().join("manifest.json");
fs::write(
&manifest_path,
parser_manifest_json(
"",
false,
FACE_PARSER_EXPECTED_SHA256,
expected_label_json(),
),
)
.unwrap();
let config = HelperBackendConfig {
python_path: PathBuf::from("python3"),
mediapipe_task_path: Some(parser_path.clone()),
face_parsing_onnx_path: Some(parser_path.clone()),
asset_manifest_path: Some(manifest_path.clone()),
timeout: DEFAULT_TIMEOUT,
};
let readiness = VisionReadinessReport {
backend_available: false,
python_version: Some("Python 3.12.0".to_string()),
missing_packages: Vec::new(),
missing_models: missing_model_assets(&config),
warnings: Vec::new(),
};
assert!(matches!(
first_readiness_error(readiness),
VisionError::UnsupportedModelLicense
));
fs::write(
&manifest_path,
parser_manifest_json(
FACE_PARSER_LICENSE,
true,
FACE_PARSER_EXPECTED_SHA256,
expected_label_json(),
),
)
.unwrap();
let readiness = VisionReadinessReport {
backend_available: false,
python_version: Some("Python 3.12.0".to_string()),
missing_packages: Vec::new(),
missing_models: missing_model_assets(&config),
warnings: Vec::new(),
};
assert!(matches!(
first_readiness_error(readiness),
VisionError::ModelChecksumMismatch
));
}
#[test]
fn parser_manifest_rejects_baseline_facial_hair_labels() {
let temp = tempfile::tempdir().unwrap();
let parser_path = temp.path().join("parser.onnx");
fs::write(&parser_path, b"parser").unwrap();
let manifest_path = temp.path().join("manifest.json");
fs::write(
&manifest_path,
parser_manifest_json(
FACE_PARSER_LICENSE,
true,
FACE_PARSER_EXPECTED_SHA256,
r#"{"background":0,"skin":1,"l_brow":2,"r_brow":3,"l_eye":4,"r_eye":5,"eye_g":6,"l_ear":7,"r_ear":8,"ear_r":9,"nose":10,"mouth":11,"u_lip":12,"l_lip":13,"neck":14,"neck_l":15,"cloth":16,"hair":17,"hat":18,"beard":19}"#,
),
)
.unwrap();
let config = HelperBackendConfig {
python_path: PathBuf::from("python3"),
mediapipe_task_path: Some(parser_path.clone()),
face_parsing_onnx_path: Some(parser_path),
asset_manifest_path: Some(manifest_path),
timeout: DEFAULT_TIMEOUT,
};
assert_eq!(
missing_model_assets(&config),
vec!["face_parsing_facial_hair_labels_rejected".to_string()]
);
}
#[test]
fn parser_manifest_rejects_wrong_provenance_metadata() {
let temp = tempfile::tempdir().unwrap();
let parser_path = temp.path().join("parser.onnx");
fs::write(&parser_path, b"parser").unwrap();
let manifest_path = temp.path().join("manifest.json");
let manifest = parser_manifest_json(
FACE_PARSER_LICENSE,
true,
FACE_PARSER_EXPECTED_SHA256,
expected_label_json(),
)
.replace(FACE_PARSER_PROVENANCE, "unknown");
fs::write(&manifest_path, manifest).unwrap();
let config = HelperBackendConfig {
python_path: PathBuf::from("python3"),
mediapipe_task_path: Some(parser_path.clone()),
face_parsing_onnx_path: Some(parser_path),
asset_manifest_path: Some(manifest_path),
timeout: DEFAULT_TIMEOUT,
};
assert!(
missing_model_assets(&config)
.iter()
.any(|missing| missing == "face_parsing_license_not_approved")
);
}
#[test]
fn onnxruntime_is_required_for_helper_readiness() {
let base_packages = required_python_packages(false)
.into_iter()
.map(|package| package.display_name)
.collect::<Vec<_>>();
let parser_packages = required_python_packages(true)
.into_iter()
.map(|package| package.display_name)
.collect::<Vec<_>>();
assert!(base_packages.contains(&"onnxruntime"));
assert!(parser_packages.contains(&"onnxruntime"));
}
#[test]
fn reads_protocol_error_response_after_nonzero_process_exit() {
let temp = tempfile::tempdir().unwrap();
let script = temp.path().join("python");
let response_path = temp.path().join("response.json");
let request_path = temp.path().join("request.json");
fs::write(&request_path, b"{}").unwrap();
let script_body = format!(
"#!/bin/sh\ncat > '{}' <<'JSON'\n{{\"schema_version\":1,\"request_id\":\"unknown\",\"status\":\"error\",\"region_set\":null,\"overlay\":null,\"error_code\":\"invalid_request\",\"warnings\":[]}}\nJSON\nexit 1\n",
response_path.display()
);
fs::write(&script, script_body).unwrap();
make_executable(&script);
let config = HelperBackendConfig {
python_path: script,
mediapipe_task_path: None,
face_parsing_onnx_path: None,
asset_manifest_path: None,
timeout: DEFAULT_TIMEOUT,
};
let process = run_helper_process(&config, &request_path, &response_path).unwrap();
assert!(!process.exited_successfully);
let response = read_helper_response(&response_path).unwrap();
assert!(matches!(
response.error_code,
Some(HelperErrorCode::InvalidRequest)
));
}
#[test]
fn parse_time_model_missing_preserves_request_id_and_typed_error() {
let temp = tempfile::tempdir().unwrap();
let request_path = temp.path().join("request.json");
let response_path = temp.path().join("response.json");
let image_path = temp.path().join("input.png");
fs::write(&image_path, b"placeholder").unwrap();
fs::write(
&request_path,
serde_json::json!({
"schema_version": HELPER_SCHEMA_VERSION,
"request_id": "req",
"image_path": image_path,
"image_width": 10,
"image_height": 10,
"requested_regions": ["skin"],
"min_region_pixels": 20,
"erode_pixels": 2,
"max_faces": 1,
"mediapipe_task_path": temp.path().join("missing.task"),
"face_parsing_onnx_path": null,
"overlay_output_path": null,
"overlay_include_points": false,
"overlay_include_labels": false
})
.to_string(),
)
.unwrap();
let config = HelperBackendConfig {
python_path: PathBuf::from("python3"),
mediapipe_task_path: None,
face_parsing_onnx_path: None,
asset_manifest_path: None,
timeout: DEFAULT_TIMEOUT,
};
let Ok(_) = run_helper_process(&config, &request_path, &response_path) else {
return;
};
let response = read_helper_response(&response_path).unwrap();
assert_eq!(response.request_id, "req");
assert!(matches!(
validate_helper_response(response, "req", 10, 10, &ExtractionRequest::default()),
Err(VisionError::ModelMissing(_))
));
}
#[test]
fn requested_hair_and_beard_observations_are_required() {
let mut missing_hair = valid_region_set(10, 10);
missing_hair
.regions
.retain(|region| region.kind != RegionKind::Hair);
assert!(matches!(
validate_helper_response(
ok_response(missing_hair),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(_))
));
let mut missing_beard = valid_region_set(10, 10);
missing_beard
.regions
.retain(|region| region.kind != RegionKind::Beard);
assert!(matches!(
validate_helper_response(
ok_response(missing_beard),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(_))
));
assert!(
validate_helper_response(
ok_response(valid_region_set(10, 10)),
"req",
10,
10,
&ExtractionRequest::default()
)
.is_ok()
);
}
#[test]
fn low_evidence_beard_observation_satisfies_requested_beard_contract() {
let mut region_set = valid_region_set(10, 10);
let beard = region_set
.regions
.iter_mut()
.find(|region| region.kind == RegionKind::Beard)
.expect("valid fixture should include beard");
beard.status = RegionStatus::LowEvidence;
beard.confidence = 0.35;
beard.approximate_reason = Some("clean_shaven_or_low_stubble_evidence".to_string());
assert!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
)
.is_ok()
);
}
#[test]
fn stale_not_configured_hair_response_is_rejected() {
let mut region_set = valid_region_set(10, 10);
let hair = region_set
.regions
.iter_mut()
.find(|region| region.kind == RegionKind::Hair)
.expect("valid fixture should include hair");
hair.status = RegionStatus::NotMeasured {
reason: "semantic_parser_not_configured".to_string(),
};
hair.not_measured_reason = Some("semantic_parser_not_configured".to_string());
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(reason))
if reason == "semantic_parser_hair_response_invalid"
));
}
#[test]
fn measured_hair_must_come_from_parser_mask() {
let mut region_set = valid_region_set(10, 10);
set_hair_measured_from_parser(&mut region_set);
assert!(
validate_helper_response(
ok_response(region_set.clone()),
"req",
10,
10,
&ExtractionRequest::default()
)
.is_ok()
);
let hair = hair_region_mut(&mut region_set);
hair.source = RegionSource::MediapipeLandmarks;
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(reason))
if reason == "semantic_parser_hair_response_invalid"
));
}
#[test]
fn not_measured_hair_requires_matching_low_evidence_reasons() {
let aligned_region_set = valid_region_set(10, 10);
assert!(
validate_helper_response(
ok_response(aligned_region_set),
"req",
10,
10,
&ExtractionRequest::default()
)
.is_ok()
);
for (status_reason, duplicated_reason) in [
(
"semantic_parser_not_configured",
Some("semantic_parser_hair_evidence_low"),
),
(
"semantic_parser_hair_evidence_low",
Some("semantic_parser_not_configured"),
),
("semantic_parser_hair_evidence_low", None),
] {
let mut region_set = valid_region_set(10, 10);
let hair = hair_region_mut(&mut region_set);
hair.status = RegionStatus::NotMeasured {
reason: status_reason.to_string(),
};
hair.not_measured_reason = duplicated_reason.map(str::to_string);
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(reason))
if reason == "semantic_parser_hair_response_invalid"
));
}
}
#[test]
fn measured_hair_rejects_stale_not_measured_reason() {
let mut region_set = valid_region_set(10, 10);
set_hair_measured_from_parser(&mut region_set);
hair_region_mut(&mut region_set).not_measured_reason =
Some("semantic_parser_not_configured".to_string());
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(reason))
if reason == "semantic_parser_hair_response_invalid"
));
}
#[test]
fn duplicate_hair_observations_are_rejected_even_when_first_is_valid() {
let mut region_set = valid_region_set(10, 10);
set_hair_measured_from_parser(&mut region_set);
let mut duplicate_hair = hair_region_mut(&mut region_set).clone();
duplicate_hair.source = RegionSource::MediapipeLandmarks;
region_set.regions.push(duplicate_hair);
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(reason))
if reason == "semantic_parser_hair_duplicate"
));
}
#[test]
fn duplicate_valid_parser_hair_observations_are_rejected() {
let mut region_set = valid_region_set(10, 10);
set_hair_measured_from_parser(&mut region_set);
let duplicate_hair = hair_region_mut(&mut region_set).clone();
region_set.regions.push(duplicate_hair);
assert!(matches!(
validate_helper_response(
ok_response(region_set),
"req",
10,
10,
&ExtractionRequest::default()
),
Err(VisionError::HelperProtocolError(reason))
if reason == "semantic_parser_hair_duplicate"
));
}
fn ok_response(region_set: RegionSet) -> HelperResponseEnvelope {
HelperResponseEnvelope {
schema_version: HELPER_SCHEMA_VERSION,
request_id: "req".to_string(),
status: HelperStatus::Ok,
region_set: Some(region_set),
overlay: None,
error_code: None,
warnings: Vec::new(),
}
}
fn error_response(error_code: HelperErrorCode) -> HelperResponseEnvelope {
HelperResponseEnvelope {
schema_version: HELPER_SCHEMA_VERSION,
request_id: "req".to_string(),
status: HelperStatus::Error,
region_set: None,
overlay: None,
error_code: Some(error_code),
warnings: Vec::new(),
}
}
fn valid_region_set(width: u32, height: u32) -> RegionSet {
let square = RegionMask::Polygon {
polygons: vec![vec![
Point { x: 0.0, y: 0.0 },
Point { x: 9.0, y: 0.0 },
Point { x: 9.0, y: 9.0 },
Point { x: 0.0, y: 9.0 },
]],
};
let region = |kind| RegionObservation {
kind,
status: RegionStatus::Measured,
source: RegionSource::MediapipeLandmarks,
confidence: 0.9,
mask: Some(square.clone()),
sample_hint: Some(20),
approximate_reason: None,
not_measured_reason: None,
};
let hair = RegionObservation {
kind: RegionKind::Hair,
status: RegionStatus::NotMeasured {
reason: "semantic_parser_hair_evidence_low".to_string(),
},
source: RegionSource::NotMeasured,
confidence: 0.0,
mask: None,
sample_hint: None,
approximate_reason: None,
not_measured_reason: Some("semantic_parser_hair_evidence_low".to_string()),
};
let beard = RegionObservation {
kind: RegionKind::Beard,
status: RegionStatus::Approximate,
source: RegionSource::Approximation,
confidence: 0.55,
mask: Some(square.clone()),
sample_hint: Some(20),
approximate_reason: Some("landmark_lower_face_proxy".to_string()),
not_measured_reason: None,
};
RegionSet {
image_width: width,
image_height: height,
regions: vec![
region(RegionKind::Skin),
region(RegionKind::Brow),
region(RegionKind::Iris),
region(RegionKind::Sclera),
region(RegionKind::Lip),
hair,
beard,
],
extraction_quality: ExtractionQualityReport {
faces_detected: 1,
selected_face_index: Some(0),
backend: "mediapipe".to_string(),
warnings: Vec::new(),
},
warnings: Vec::new(),
}
}
fn hair_region_mut(region_set: &mut RegionSet) -> &mut RegionObservation {
region_set
.regions
.iter_mut()
.find(|region| region.kind == RegionKind::Hair)
.expect("valid fixture should include hair")
}
fn set_hair_measured_from_parser(region_set: &mut RegionSet) {
let square = RegionMask::Polygon {
polygons: vec![vec![
Point { x: 0.0, y: 0.0 },
Point { x: 9.0, y: 0.0 },
Point { x: 9.0, y: 9.0 },
Point { x: 0.0, y: 9.0 },
]],
};
let hair = hair_region_mut(region_set);
hair.status = RegionStatus::Measured;
hair.source = RegionSource::ParserMask;
hair.confidence = 0.78;
hair.mask = Some(square);
hair.sample_hint = Some(20);
hair.not_measured_reason = None;
}
fn make_executable(path: &Path) {
let mut permissions = fs::metadata(path).unwrap().permissions();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
permissions.set_mode(0o700);
}
fs::set_permissions(path, permissions).unwrap();
}
}