django-camera-kit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. django_camera_kit/__init__.py +1 -0
  2. django_camera_kit/apps.py +7 -0
  3. django_camera_kit/kyc/__init__.py +0 -0
  4. django_camera_kit/kyc/apps.py +8 -0
  5. django_camera_kit/kyc/conf.py +24 -0
  6. django_camera_kit/kyc/embeddings.py +39 -0
  7. django_camera_kit/kyc/migrations/0001_initial.py +85 -0
  8. django_camera_kit/kyc/migrations/__init__.py +0 -0
  9. django_camera_kit/kyc/models.py +67 -0
  10. django_camera_kit/kyc/serializers.py +23 -0
  11. django_camera_kit/kyc/static/django_camera_kit/kyc/css/kyc_capture.css +59 -0
  12. django_camera_kit/kyc/static/django_camera_kit/kyc/js/face_kit.js +116 -0
  13. django_camera_kit/kyc/static/django_camera_kit/kyc/js/kyc_capture.js +347 -0
  14. django_camera_kit/kyc/static/django_camera_kit/kyc/vendor/face_detection_yunet_2023mar.onnx +0 -0
  15. django_camera_kit/kyc/templates/django_camera_kit/kyc/widgets/kyc_verification.html +7 -0
  16. django_camera_kit/kyc/throttling.py +20 -0
  17. django_camera_kit/kyc/urls.py +9 -0
  18. django_camera_kit/kyc/views.py +65 -0
  19. django_camera_kit/kyc/widgets.py +39 -0
  20. django_camera_kit/static/django_camera_kit/css/camera_kit.css +97 -0
  21. django_camera_kit/static/django_camera_kit/js/camera_kit.js +213 -0
  22. django_camera_kit/static/django_camera_kit/js/doc_scan.js +367 -0
  23. django_camera_kit/static/django_camera_kit/vendor/README.md +6 -0
  24. django_camera_kit/static/django_camera_kit/vendor/jspdf.umd.min.js +373 -0
  25. django_camera_kit/static/django_camera_kit/vendor/opencv.js +0 -0
  26. django_camera_kit/templates/django_camera_kit/widgets/document_scanner.html +6 -0
  27. django_camera_kit/widgets.py +25 -0
  28. django_camera_kit-0.1.0.dist-info/METADATA +129 -0
  29. django_camera_kit-0.1.0.dist-info/RECORD +32 -0
  30. django_camera_kit-0.1.0.dist-info/WHEEL +5 -0
  31. django_camera_kit-0.1.0.dist-info/licenses/LICENSE +21 -0
  32. django_camera_kit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoCameraKitConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "django_camera_kit"
7
+ verbose_name = "Camera Kit"
File without changes
@@ -0,0 +1,8 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class DjangoCameraKitKycConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "django_camera_kit.kyc"
7
+ label = "camera_kit_kyc"
8
+ verbose_name = "Camera Kit — KYC"
@@ -0,0 +1,24 @@
1
+ from django.conf import settings
2
+
3
+ DEFAULTS = {
4
+ # insightface model pack — see embeddings.py. Changing this to a model
5
+ # with a different embedding size requires a migration (see models.py).
6
+ "EMBEDDING_MODEL": "buffalo_sc",
7
+ # Cosine similarity threshold above which a selfie/ID pair is considered
8
+ # a match. This default is a starting point, not a calibrated value —
9
+ # tune it against real data before relying on it in production.
10
+ "MATCH_THRESHOLD": 0.45,
11
+ # Max requests per authenticated user against KYCVerifyView. Each request
12
+ # runs a full face-embedding inference pass — without a limit the
13
+ # endpoint is a cheap DoS/probing vector. DRF throttle-rate syntax
14
+ # ("N/period" — second/minute/hour/day).
15
+ "THROTTLE_RATE": "10/hour",
16
+ # Reject selfie/id_document uploads larger than this, before they're
17
+ # saved or processed.
18
+ "MAX_UPLOAD_SIZE_MB": 8,
19
+ }
20
+
21
+
22
+ def get_setting(name):
23
+ user_settings = getattr(settings, "DJANGO_CAMERA_KIT_KYC", {})
24
+ return user_settings.get(name, DEFAULTS[name])
@@ -0,0 +1,39 @@
1
+ import numpy as np
2
+
3
+ from . import conf
4
+
5
+ _face_app = None
6
+
7
+
8
+ def _get_face_app():
9
+ global _face_app
10
+ if _face_app is None:
11
+ from insightface.app import FaceAnalysis
12
+
13
+ _face_app = FaceAnalysis(
14
+ name=conf.get_setting("EMBEDDING_MODEL"),
15
+ providers=["CPUExecutionProvider"],
16
+ )
17
+ _face_app.prepare(ctx_id=-1, det_size=(320, 320))
18
+ return _face_app
19
+
20
+
21
+ def extract_face_embedding(image_bgr):
22
+ """Return the normalized embedding of the largest face in the image,
23
+ or None if no face was detected — also if `image_bgr` is None, which
24
+ is what cv2.imdecode() returns for corrupt/malformed image data that
25
+ nonetheless passed DRF's ImageField validation."""
26
+ if image_bgr is None:
27
+ return None
28
+ faces = _get_face_app().get(image_bgr)
29
+ if not faces:
30
+ return None
31
+ largest = max(
32
+ faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])
33
+ )
34
+ return largest.normed_embedding
35
+
36
+
37
+ def compare_embeddings(embedding_a, embedding_b):
38
+ """Cosine similarity between two normalized embeddings, in [-1, 1]."""
39
+ return float(np.dot(embedding_a, embedding_b))
@@ -0,0 +1,85 @@
1
+ # Generated by Django 5.0.14 on 2026-07-10 10:22
2
+
3
+ import django.db.models.deletion
4
+ import pgvector.django.indexes
5
+ import pgvector.django.vector
6
+ from django.conf import settings
7
+ from django.db import migrations, models
8
+ from pgvector.django import VectorExtension
9
+
10
+
11
+ class Migration(migrations.Migration):
12
+ initial = True
13
+
14
+ dependencies = [
15
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16
+ ]
17
+
18
+ operations = [
19
+ VectorExtension(),
20
+ migrations.CreateModel(
21
+ name="KYCVerification",
22
+ fields=[
23
+ (
24
+ "id",
25
+ models.BigAutoField(
26
+ auto_created=True,
27
+ primary_key=True,
28
+ serialize=False,
29
+ verbose_name="ID",
30
+ ),
31
+ ),
32
+ ("selfie", models.ImageField(upload_to="camera_kit/kyc/selfies/")),
33
+ (
34
+ "id_document",
35
+ models.ImageField(upload_to="camera_kit/kyc/id_documents/"),
36
+ ),
37
+ (
38
+ "selfie_embedding",
39
+ pgvector.django.vector.VectorField(
40
+ blank=True, dimensions=512, null=True
41
+ ),
42
+ ),
43
+ (
44
+ "id_face_embedding",
45
+ pgvector.django.vector.VectorField(
46
+ blank=True, dimensions=512, null=True
47
+ ),
48
+ ),
49
+ ("match_score", models.FloatField(blank=True, null=True)),
50
+ ("liveness_passed", models.BooleanField(default=False)),
51
+ (
52
+ "status",
53
+ models.CharField(
54
+ choices=[
55
+ ("pending", "En attente"),
56
+ ("verified", "Vérifié"),
57
+ ("rejected", "Rejeté"),
58
+ ],
59
+ default="pending",
60
+ max_length=16,
61
+ ),
62
+ ),
63
+ ("created_at", models.DateTimeField(auto_now_add=True)),
64
+ (
65
+ "user",
66
+ models.ForeignKey(
67
+ on_delete=django.db.models.deletion.CASCADE,
68
+ related_name="kyc_verifications",
69
+ to=settings.AUTH_USER_MODEL,
70
+ ),
71
+ ),
72
+ ],
73
+ options={
74
+ "indexes": [
75
+ pgvector.django.indexes.HnswIndex(
76
+ ef_construction=64,
77
+ fields=["selfie_embedding"],
78
+ m=16,
79
+ name="camera_kit_kyc_selfie_hnsw",
80
+ opclasses=["vector_cosine_ops"],
81
+ )
82
+ ],
83
+ },
84
+ ),
85
+ ]
File without changes
@@ -0,0 +1,67 @@
1
+ from django.conf import settings
2
+ from django.db import models
3
+ from pgvector.django import HnswIndex, VectorField
4
+
5
+ # Matches buffalo_sc's w600k_mbf recognition output (see embeddings.py).
6
+ # Changing the embedding model to one with a different output size requires
7
+ # a new migration.
8
+ EMBEDDING_DIMENSIONS = 512
9
+
10
+
11
+ class KYCVerification(models.Model):
12
+ class Status(models.TextChoices):
13
+ PENDING = "pending", "En attente"
14
+ VERIFIED = "verified", "Vérifié"
15
+ REJECTED = "rejected", "Rejeté"
16
+
17
+ # Every verification is bound to the authenticated user who submitted
18
+ # it (KYCVerifyView requires authentication) — a verification result
19
+ # is meaningless without knowing whose identity it claims to confirm.
20
+ user = models.ForeignKey(
21
+ settings.AUTH_USER_MODEL,
22
+ on_delete=models.CASCADE,
23
+ related_name="kyc_verifications",
24
+ )
25
+
26
+ # selfie/id_document contain biometric + government-ID imagery. The
27
+ # consuming project MUST point Django's storage backend at a private,
28
+ # access-controlled location for these — never a publicly readable
29
+ # MEDIA_URL. django-camera-kit doesn't enforce this itself since
30
+ # storage config is the consuming project's responsibility, but the
31
+ # API response (KYCVerificationSerializer) deliberately never exposes
32
+ # these files' URLs.
33
+ selfie = models.ImageField(upload_to="camera_kit/kyc/selfies/")
34
+ id_document = models.ImageField(upload_to="camera_kit/kyc/id_documents/")
35
+
36
+ selfie_embedding = VectorField(
37
+ dimensions=EMBEDDING_DIMENSIONS, null=True, blank=True
38
+ )
39
+ id_face_embedding = VectorField(
40
+ dimensions=EMBEDDING_DIMENSIONS, null=True, blank=True
41
+ )
42
+ match_score = models.FloatField(null=True, blank=True)
43
+
44
+ # MVP liveness only: client-asserted result of the in-browser
45
+ # head-movement challenge. Not a server-verified anti-spoofing signal —
46
+ # a malicious client could send True without ever performing it. Real
47
+ # server-side liveness verification is a later phase.
48
+ liveness_passed = models.BooleanField(default=False)
49
+
50
+ status = models.CharField(
51
+ max_length=16, choices=Status.choices, default=Status.PENDING
52
+ )
53
+ created_at = models.DateTimeField(auto_now_add=True)
54
+
55
+ class Meta:
56
+ indexes = [
57
+ HnswIndex(
58
+ name="camera_kit_kyc_selfie_hnsw",
59
+ fields=["selfie_embedding"],
60
+ m=16,
61
+ ef_construction=64,
62
+ opclasses=["vector_cosine_ops"],
63
+ ),
64
+ ]
65
+
66
+ def __str__(self):
67
+ return f"KYCVerification #{self.pk} ({self.status})"
@@ -0,0 +1,23 @@
1
+ from rest_framework import serializers
2
+
3
+ from . import conf
4
+ from .models import KYCVerification
5
+
6
+
7
+ def validate_upload_size(file):
8
+ max_mb = conf.get_setting("MAX_UPLOAD_SIZE_MB")
9
+ if file.size > max_mb * 1024 * 1024:
10
+ raise serializers.ValidationError(f"Fichier trop volumineux (max {max_mb} Mo).")
11
+
12
+
13
+ class KYCSubmissionSerializer(serializers.Serializer):
14
+ selfie = serializers.ImageField(validators=[validate_upload_size])
15
+ id_document = serializers.ImageField(validators=[validate_upload_size])
16
+ liveness_passed = serializers.BooleanField(default=False)
17
+
18
+
19
+ class KYCVerificationSerializer(serializers.ModelSerializer):
20
+ class Meta:
21
+ model = KYCVerification
22
+ fields = ["id", "status", "match_score", "liveness_passed", "created_at"]
23
+ read_only_fields = fields
@@ -0,0 +1,59 @@
1
+ .camera-kit-kyc {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 0.5rem;
5
+ flex-wrap: wrap;
6
+ }
7
+
8
+ /* ISO/IEC 7810 ID-1 format (85.60 x 53.98mm) — the size of virtually every
9
+ national ID card, driving license and payment card. The ratio is fixed
10
+ via aspect-ratio so it holds regardless of the camera's own aspect
11
+ ratio, instead of an arbitrary box that doesn't match a real card. */
12
+ .camera-kit-kyc-id-guide {
13
+ position: absolute;
14
+ top: 50%;
15
+ left: 50%;
16
+ width: 75%;
17
+ aspect-ratio: 85.6 / 53.98;
18
+ transform: translate(-50%, -50%);
19
+ border: 3px dashed rgba(255, 255, 255, 0.8);
20
+ border-radius: 0.5rem;
21
+ pointer-events: none;
22
+ }
23
+
24
+ .camera-kit-kyc-face-guide {
25
+ position: absolute;
26
+ top: 12%;
27
+ left: 30%;
28
+ right: 30%;
29
+ bottom: 12%;
30
+ border: 3px dashed rgba(255, 255, 255, 0.8);
31
+ border-radius: 50%;
32
+ pointer-events: none;
33
+ }
34
+
35
+ .camera-kit-kyc-hint {
36
+ text-align: center;
37
+ padding: 0.5rem 1rem;
38
+ margin: 0;
39
+ font-weight: 600;
40
+ }
41
+
42
+ .camera-kit-kyc-review {
43
+ display: flex;
44
+ gap: 1rem;
45
+ padding: 1rem;
46
+ justify-content: center;
47
+ }
48
+
49
+ .camera-kit-kyc-review img {
50
+ max-height: 180px;
51
+ border-radius: 0.25rem;
52
+ border: 1px solid #dee2e6;
53
+ }
54
+
55
+ .camera-kit-kyc-review-status,
56
+ .camera-kit-kyc-result {
57
+ text-align: center;
58
+ padding: 0 1rem;
59
+ }
@@ -0,0 +1,116 @@
1
+ (function (window, document) {
2
+ "use strict";
3
+
4
+ // Locate vendor/ relative to this script's own URL so it works under any
5
+ // STATIC_URL prefix, instead of hardcoding a path.
6
+ var scriptUrl = document.currentScript && document.currentScript.src;
7
+ var vendorBase = scriptUrl ? scriptUrl.replace(/js\/face_kit\.js.*$/, "vendor/") : "";
8
+ var MODEL_FILE = "face_detection_yunet_2023mar.onnx";
9
+
10
+ var detector = null;
11
+ var readyPromise = null;
12
+
13
+ function loadModelFile(cv) {
14
+ return fetch(vendorBase + MODEL_FILE)
15
+ .then(function (r) {
16
+ return r.arrayBuffer();
17
+ })
18
+ .then(function (buf) {
19
+ cv.FS_createDataFile("/", MODEL_FILE, new Uint8Array(buf), true, false, false);
20
+ });
21
+ }
22
+
23
+ // Reuses the opencv.js instance already loaded by CameraKit (camera_kit.js
24
+ // must run before this file) instead of vendoring a second CV stack.
25
+ //
26
+ // Uses YuNet (cv.FaceDetectorYN), a small CNN trained on WIDER FACE —
27
+ // deliberately not a Haar cascade. Haar cascades detect faces from
28
+ // brightness-contrast patterns and are well documented to have far
29
+ // higher miss rates on darker skin tones and low-contrast lighting
30
+ // (see e.g. Buolamwini & Gebru, "Gender Shades", 2018). YuNet doesn't
31
+ // carry that specific bias.
32
+ function ready() {
33
+ if (readyPromise) return readyPromise;
34
+
35
+ readyPromise = window.CameraKit.ready().then(function (cvIsReady) {
36
+ if (!cvIsReady) return false;
37
+ var cv = window.CameraKit.getCv();
38
+ if (!cv.FaceDetectorYN) return false;
39
+
40
+ return loadModelFile(cv).then(function () {
41
+ // This embind binding exposes the C++ static FaceDetectorYN::create()
42
+ // factory as a plain constructor, not a .create() static method.
43
+ detector = new cv.FaceDetectorYN(
44
+ MODEL_FILE,
45
+ "",
46
+ new cv.Size(320, 320),
47
+ 0.8,
48
+ 0.3,
49
+ 5000,
50
+ 0,
51
+ 0
52
+ );
53
+ return true;
54
+ });
55
+ });
56
+
57
+ return readyPromise;
58
+ }
59
+
60
+ // Largest detected face as {x, y, width, height, centerX, centerY, score}
61
+ // in canvas pixel coordinates, or null if none found.
62
+ function detectFace(canvas) {
63
+ var cv = window.CameraKit.getCv();
64
+ if (!cv || !detector) return null;
65
+
66
+ detector.setInputSize(new cv.Size(canvas.width, canvas.height));
67
+
68
+ var src = cv.imread(canvas);
69
+ var bgr = new cv.Mat();
70
+ var faces = new cv.Mat();
71
+ var result = null;
72
+
73
+ try {
74
+ cv.cvtColor(src, bgr, cv.COLOR_RGBA2BGR);
75
+ detector.detect(bgr, faces);
76
+
77
+ if (faces.rows > 0) {
78
+ var bestRow = 0;
79
+ var bestArea = 0;
80
+ for (var i = 0; i < faces.rows; i++) {
81
+ var area = faces.floatAt(i, 2) * faces.floatAt(i, 3);
82
+ if (area > bestArea) {
83
+ bestArea = area;
84
+ bestRow = i;
85
+ }
86
+ }
87
+
88
+ var x = faces.floatAt(bestRow, 0);
89
+ var y = faces.floatAt(bestRow, 1);
90
+ var width = faces.floatAt(bestRow, 2);
91
+ var height = faces.floatAt(bestRow, 3);
92
+
93
+ result = {
94
+ x: x,
95
+ y: y,
96
+ width: width,
97
+ height: height,
98
+ centerX: x + width / 2,
99
+ centerY: y + height / 2,
100
+ score: faces.floatAt(bestRow, 14),
101
+ };
102
+ }
103
+ } finally {
104
+ src.delete();
105
+ bgr.delete();
106
+ faces.delete();
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ window.FaceKit = {
113
+ ready: ready,
114
+ detectFace: detectFace,
115
+ };
116
+ })(window, document);