Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
42 changes: 0 additions & 42 deletions ynr/apps/moderation_queue/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,8 @@
from tempfile import NamedTemporaryFile

import requests
from candidates.models.db import ActionType, LoggedAction
from candidates.views.version_data import get_client_ip
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
from PIL import Image as PillowImage

from .models import QueuedImage


def upload_photo_response(request, person, image_form, url_form):
return render(
request,
"moderation_queue/photo-upload-new.html",
{
"image_form": image_form,
"url_form": url_form,
"queued_images": QueuedImage.objects.filter(
person=person, decision="undecided"
).order_by("created"),
"person": person,
},
)


def image_form_valid_response(request, person, image_form):
# Make sure that we save the user that made the upload
queued_image = image_form.save(commit=False)
queued_image.user = request.user
queued_image.save()
# Record that action:
LoggedAction.objects.create(
user=request.user,
action_type=ActionType.PHOTO_UPLOAD,
ip_address=get_client_ip(request),
popit_person_new_version="",
person=person,
source=image_form.cleaned_data["justification_for_use"],
)
return HttpResponseRedirect(
reverse("photo-upload-success", kwargs={"person_id": person.id})
)


# 15MB — Rekognition's S3 object size limit
MAX_IMAGE_BYTES = 15 * 1024 * 1024

Expand Down

This file was deleted.

83 changes: 83 additions & 0 deletions ynr/apps/moderation_queue/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import ast
import json
import uuid
from datetime import date
from os.path import join, splitext
from tempfile import NamedTemporaryFile

import sorl.thumbnail
from django.contrib.auth.models import User
from django.db import models
from django.urls import reverse
from PIL import Image as PillowImage
from PIL import ImageOps

from .helpers import convert_image_to_png

PHOTO_REVIEWERS_GROUP_NAME = "Photo Reviewers"
VERY_TRUSTED_USER_GROUP_NAME = "Very Trusted User"
Expand Down Expand Up @@ -126,6 +131,84 @@ def uploaded_by(self):
return self.user.username
return "a robot 🤖"

def start_image_processing(self):
from django_q.tasks import async_chain
Comment thread
chris48s marked this conversation as resolved.
Outdated

async_chain(
Copy link
Copy Markdown
Member

@chris48s chris48s Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to explicitly set a timeout on these two tasks. Our default timeout for the cluster is 240 seconds. We need this to be quite long for our scheduled tasks which are processing many objects in a single run, but we should set a shorter timeout on these jobs that are only processing a single object.

Also a quick note on retries: Our cluster is set not to retry failed tasks. We basically need that to be true at the moment with all the tasks we have running on a short loop. We can't configure this at a task level, so if this fails once, it won't retry. I don't think that is a huge issue, but worth being aware of. Once we have fewer scheduled tasks running frequently, we can look at changing that. I think as it stands this is no worse than what we do now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still stands

[
(
"moderation_queue.tasks.normalise_queued_image",
(self.id,),
),
(
"moderation_queue.tasks.detect_faces_for_queued_image",
(self.id,),
),
]
)

def normalise_image(self):
pil_img = PillowImage.open(self.image.file)
pil_img = ImageOps.exif_transpose(pil_img)
png_buffer = convert_image_to_png(pil_img)
self.image.save(self.image.name, png_buffer, save=False)
sorl.thumbnail.delete(self.image.name, delete_file=False)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here where we clear the thumnail cache, self.image.name is now the new .png name so we're clearing the cache for the new .png name but not the old .jpg name. That seems unintended.


def _face_crop_bound(self, bound, im_size, scaling_factor):
return max(0, bound * im_size * scaling_factor)

def _apply_face_detection(self, detected):
if not (detected and detected.get("FaceDetails")):
return
# AWS crops faces tightly by default. these scaling factors give a
# slightly wider crop that includes more context around the face.
MIN_SCALING_FACTOR = 0.7
MAX_SCALING_FACTOR = 1.3
bb = detected["FaceDetails"][0]["BoundingBox"]
self.crop_min_x = self._face_crop_bound(
bb["Left"], self.image.width, MIN_SCALING_FACTOR
)
self.crop_min_y = self._face_crop_bound(
bb["Top"], self.image.height, MIN_SCALING_FACTOR
)
self.crop_max_x = self._face_crop_bound(
bb["Width"], self.image.width, MAX_SCALING_FACTOR
)
self.crop_max_y = self._face_crop_bound(
bb["Height"], self.image.height, MAX_SCALING_FACTOR
)
self.detection_metadata = json.dumps(detected, indent=4)

def detect_faces(self):
import boto3
Comment thread
chris48s marked this conversation as resolved.
Outdated

try:
from storages.backends.s3 import S3Storage
except ImportError:
S3Storage = None

try:
rekognition = boto3.client("rekognition", region_name="eu-west-1")
storage = self.image.storage
if S3Storage and isinstance(storage, S3Storage):
rekognition_image = {
"S3Object": {
"Bucket": storage.bucket_name,
"Name": storage._normalize_name(self.image.name),
}
}
else:
with self.image.open("rb") as f:
rekognition_image = {"Bytes": f.read()}
detected = rekognition.detect_faces(
Image=rekognition_image, Attributes=["ALL"]
)
self._apply_face_detection(detected)
finally:
self.face_detection_tried = True
self.rotation_tried = True
self.save()

def crop_image(self):
"""
Returns a temporary file containing the cropped image
Expand Down
11 changes: 11 additions & 0 deletions ynr/apps/moderation_queue/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from moderation_queue.models import QueuedImage


def normalise_queued_image(queued_image_id):
qi = QueuedImage.objects.get(pk=queued_image_id)
qi.normalise_image()


def detect_faces_for_queued_image(queued_image_id):
qi = QueuedImage.objects.get(pk=queued_image_id)
qi.detect_faces()
Loading
Loading