Реализация AI-модерации контента (изображения) в мобильном приложении
Изображения модерировать сложнее текста. Пользователь пытается обойти фильтры: редактирует фото, меняет разрешение, добавляет наклейки поверх проблемного контента. Надёжная система — многоуровневая: клиент, сервер, асинхронная проверка, хэш-база известного контента.
Почему нельзя полагаться только на одну проверку
Единственный слой — хэш-сравнение PhotoDNA / CSAM Hash — хорош для детекции известного контента, но не нового. Единственный слой — Vision API — можно обойти лёгкой обработкой изображения. Нужна комбинация.
Клиентская проверка: Vision Safe Search
На iOS VNClassifyImageRequest содержит категории типа explicit, но они не дают достаточно точного сигнала. Лучший on-device вариант — CoreML модель класса NudeNet-mobile (открытая, ~8 MB):
class LocalImageModerator {
private let model: NudeNetMobile
func check(_ image: CGImage) throws -> LocalModerationResult {
let resized = resize(image, to: CGSize(width: 320, height: 320))
let input = NudeNetInput(image: MLMultiArray(from: resized))
let output = try model.prediction(input: input)
// Классы: SAFE / EXPOSED_BREAST / EXPOSED_GENITALIA / etc.
let topClass = output.classLabels.max(by: { output.classProbability[$0]! < output.classProbability[$1]! })!
return LocalModerationResult(
isSafe: topClass == "SAFE",
confidence: output.classProbability[topClass]!
)
}
}
Время инференса — 30–50 ms на iPhone 13. Применяем ДО загрузки на сервер: если клиентская модель блокирует — не тратим bandwidth и деньги на серверную проверку.
Серверная модерация: AWS Rekognition
AWS Rekognition DetectModerationLabels — стандарт для промышленных систем. Хорошая точность, поддержка иерархических меток:
# Backend
import boto3
rekognition = boto3.client('rekognition', region_name='eu-west-1')
def moderate_image(s3_bucket: str, s3_key: str) -> ModerationResult:
response = rekognition.detect_moderation_labels(
Image={'S3Object': {'Bucket': s3_bucket, 'Name': s3_key}},
MinConfidence=60.0
)
labels = response['ModerationLabels']
top_level = [l for l in labels if not l.get('ParentName')]
blocked_categories = {'Explicit Nudity', 'Violence', 'Visually Disturbing'}
for label in top_level:
if label['Name'] in blocked_categories and label['Confidence'] > 80:
return ModerationResult(blocked=True, reason=label['Name'],
confidence=label['Confidence'])
return ModerationResult(blocked=False)
Цена: ~$0.001 за изображение (1000 запросов = $1). Для приложения с активным UGC — существенная статья расходов; оптимизируем через клиентский pre-check.
Google Cloud Vision Safe Search — альтернатива с аналогичной ценой. Возвращает adult, violence, racy, spoof, medical как вероятности от VERY_UNLIKELY до VERY_LIKELY.
PhotoDNA / Hash-based детекция CSAM
Для приложений с публичным UGC — юридическое требование в ряде юрисдикций. Microsoft PhotoDNA SDK и аналогичные решения — это перцептивный хэшинг, устойчивый к обрезке, масштабированию и небольшому сжатию. Хэш сравнивается с базой известного противоправного контента (NCMEC Hash Database в США).
Интеграция — обычно через партнёрство с NCMEC или Microsoft. Не опенсорс. Для российского рынка — IWF (Internet Watch Foundation) предоставляет аналогичный сервис.
Простой перцептивный хэш (pHash) для дедупликации — доступен через open-source:
// Android: pHash через dcperceptualhash
fun computePHash(bitmap: Bitmap): Long {
val scaled = Bitmap.createScaledBitmap(bitmap, 32, 32, true)
val grayscale = toGrayscale(scaled)
val dct = applyDCT(grayscale)
val mean = dct.average()
return dct.foldIndexed(0L) { i, acc, v -> if (v > mean) acc or (1L shl i) else acc }
}
// Hamming distance <= 10 = похожие изображения
fun hammingDistance(a: Long, b: Long): Int = java.lang.Long.bitCount(a xor b)
Асинхронная проверка и ретроактивное удаление
Синхронная модерация при загрузке — необходима, но недостаточно. Добавляем асинхронную:
- Изображение прошло синхронную проверку → опубликовано
- Асинхронно: более тяжёлая модель (GPT-4 Vision, дорогой endpoint Rekognition) перепроверяет
- Если флаг — контент помечается для ручной проверки или автоудаляется
Для высоконагруженных приложений — отдельная очередь SQS/RabbitMQ для асинхронной модерации, worker-процессы.
UX при блокировке
Пользователь должен понимать, почему фото отклонено, и иметь возможность обжаловать:
// iOS: показываем экран с причиной и кнопкой аппеляции
struct ModerationRejectionView: View {
let reason: ModerationReason
var body: some View {
VStack {
Image(systemName: "exclamationmark.triangle")
Text("Фото не соответствует правилам сообщества")
Text(reason.userFriendlyDescription)
.foregroundStyle(.secondary)
Button("Обжаловать") { /* открыть форму аппеляции */ }
Button("Выбрать другое фото") { /* dismiss */ }
}
}
}
Аппеляция — форма с текстовым полем, идёт в систему тикетов для ручной модерации. Отвечаем в течение 24–48 часов.
Сроки
Backend с Rekognition + клиентский CoreML pre-check — 4–6 дней. Полная система с pHash, асинхронной проверкой, аппеляциями и аналитикой — 3–4 недели. Стоимость зависит от объёма контента и требований к точности.







