Skip to content
This repository was archived by the owner on Sep 10, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ RUN AI_PRIVACY_ENABLED=true \
AI_PRIVACY_USE_HALF=false \
AI_PRIVACY_ME_IMAGE_PATH=me.jpeg \
python -c \
"from app.services.ai.filter import close_privacy_filter, initialize_privacy_runtime; initialize_privacy_runtime(); close_privacy_filter()"
"from pathlib import Path; from app.services.ai.filter import close_privacy_filter, initialize_privacy_runtime, prepare_identity_update; initialize_privacy_runtime(); prepare_identity_update(Path('me.jpeg')); close_privacy_filter()"

EXPOSE 8000
EXPOSE 50000-60000/udp
Expand Down
19 changes: 15 additions & 4 deletions app/services/ai/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ def register_reference_face(image_data: bytes) -> ReferenceFaceInfo:
prepared_update = filter_module.prepare_identity_update(
candidate_path
)
except _identity_validation_errors() as exc:
except _invalid_reference_image_error() as exc:
raise BadRequestError(
"유효하지 않은 이미지 파일입니다.",
details={"reason": "invalid_image"},
) from exc
except _face_not_detected_error() as exc:
raise BadRequestError(
"업로드한 이미지에서 얼굴을 감지할 수 없습니다.",
details={"reason": "face_not_detected"},
Expand Down Expand Up @@ -107,10 +112,16 @@ def delete_reference_face() -> None:
logger.info("Deleted reference face")


def _identity_validation_errors() -> tuple[type[Exception], ...]:
from privacy_blur.errors import PrivacyBlurNotReadyError
def _invalid_reference_image_error() -> type[Exception]:
from privacy_blur.errors import InvalidReferenceImageError

return InvalidReferenceImageError


def _face_not_detected_error() -> type[Exception]:
from privacy_blur.errors import ReferenceFaceNotDetectedError

return (PrivacyBlurNotReadyError, RuntimeError, ValueError)
return ReferenceFaceNotDetectedError


def _validate_image(image_data: bytes) -> None:
Expand Down
4 changes: 4 additions & 0 deletions privacy_blur/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,21 @@
)
from .config import PrivacyBlurConfig
from .errors import (
InvalidReferenceImageError,
PrivacyBlurError,
PrivacyBlurNotReadyError,
ReferenceFaceNotDetectedError,
UnsupportedFrameTypeError,
)

__all__ = [
"FacePrivacyEngine",
"FacePrivacyFilter",
"InvalidReferenceImageError",
"PrivacyBlurConfig",
"PrivacyBlurError",
"PrivacyBlurNotReadyError",
"ReferenceFaceNotDetectedError",
"UnsupportedFrameTypeError",
"configure",
"get_default_config",
Expand Down
8 changes: 8 additions & 0 deletions privacy_blur/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,13 @@ class PrivacyBlurNotReadyError(PrivacyBlurError):
"""Raised when models or runtime dependencies are missing."""


class InvalidReferenceImageError(PrivacyBlurError):
"""Raised when a reference image cannot be decoded."""


class ReferenceFaceNotDetectedError(PrivacyBlurError):
"""Raised when a reference image does not contain a detectable face."""


class UnsupportedFrameTypeError(PrivacyBlurError):
"""Raised when a frame type cannot be processed."""
28 changes: 21 additions & 7 deletions privacy_blur/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
from ultralytics import YOLO

from .config import PrivacyBlurConfig
from .errors import PrivacyBlurNotReadyError
from .errors import (
InvalidReferenceImageError,
PrivacyBlurNotReadyError,
ReferenceFaceNotDetectedError,
)
from .face_detection import detect_best_yunet_face

if TYPE_CHECKING:
from .core import FacePrivacyEngine, FaceTrack
Expand Down Expand Up @@ -234,18 +239,27 @@ def _create_ort_session(self, path: Path, kind: str) -> ort.InferenceSession:
def _load_reference_feature(self) -> torch.Tensor:
reference = cv2.imread(str(self.config.me_image_path))
if reference is None:
raise PrivacyBlurNotReadyError(
raise InvalidReferenceImageError(
f"Failed to read reference image: {self.config.me_image_path}"
)
detections = self._predict_yolo([reference])[0]
if not detections:
raise PrivacyBlurNotReadyError(

detector = cv2.FaceDetectorYN.create(
str(self.config.face_detection_model_path),
"",
(self.config.identity_detector_size,) * 2,
self.config.identity_detector_confidence,
0.3,
5000,
)
face = detect_best_yunet_face(detector, reference)
if face is None:
raise ReferenceFaceNotDetectedError(
f"No face found in reference image: {self.config.me_image_path}"
)
box, _ = max(detections, key=lambda item: item[0][2] * item[0][3] * item[1])
box = face[:4]
embeddings, valid = self._face_embeddings([(reference, box)])
if not bool(valid[0]):
raise PrivacyBlurNotReadyError(
raise ReferenceFaceNotDetectedError(
f"YuNet could not align the face in reference image: {self.config.me_image_path}"
)
return embeddings[0].detach()
Expand Down
3 changes: 2 additions & 1 deletion tests/test_deployment_privacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def test_production_compose_forces_privacy_filter_enabled():
def test_docker_image_build_validates_privacy_filter_initialization():
dockerfile = (PROJECT_ROOT / "Dockerfile").read_text()

assert "initialize_privacy_runtime; initialize_privacy_runtime()" in dockerfile
assert "initialize_privacy_runtime()" in dockerfile
assert "prepare_identity_update(Path('me.jpeg'))" in dockerfile


def test_docker_image_installs_matching_cuda_torchvision():
Expand Down
20 changes: 17 additions & 3 deletions tests/test_reference_face_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ def test_register_uses_candidate_then_publishes_final_path(running_runtime):


def test_invalid_replacement_preserves_existing_reference(running_runtime):
from privacy_blur.errors import PrivacyBlurNotReadyError
from privacy_blur.errors import ReferenceFaceNotDetectedError

REFERENCE_FACE_DIR.mkdir(parents=True, exist_ok=True)
REFERENCE_FACE_PATH.write_bytes(b"old valid image")
running_runtime["runtime"].prepare_identity.side_effect = PrivacyBlurNotReadyError(
"No face found"
running_runtime["runtime"].prepare_identity.side_effect = (
ReferenceFaceNotDetectedError("No face found")
)

with pytest.raises(BadRequestError, match="얼굴을 감지할 수 없습니다"):
Expand All @@ -96,6 +96,20 @@ def test_invalid_replacement_preserves_existing_reference(running_runtime):
running_runtime["filter"].replace_config.assert_not_called()


def test_runtime_failure_is_not_reported_as_face_not_detected(running_runtime):
REFERENCE_FACE_DIR.mkdir(parents=True, exist_ok=True)
REFERENCE_FACE_PATH.write_bytes(b"old valid image")
running_runtime["runtime"].prepare_identity.side_effect = RuntimeError(
"CUDA execution failed"
)

with pytest.raises(RuntimeError, match="CUDA execution failed"):
register_reference_face(b"replacement image")

assert REFERENCE_FACE_PATH.read_bytes() == b"old valid image"
running_runtime["runtime"].apply_identity.assert_not_called()


def test_register_without_runtime_validates_and_saves_file():
image_data = _create_test_jpeg()
with patch("app.services.ai.identity._validate_image") as validate:
Expand Down
Loading