Merge pull request #3 from SlimusMinus/fix-code_v2

fixed code v2
This commit is contained in:
SlimusMinus
2026-08-06 23:46:29 +03:00
committed by GitHub
7 changed files with 598 additions and 7 deletions

2
.env
View File

@@ -15,3 +15,5 @@ TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier
MODEL_CACHE_DIR=./models MODEL_CACHE_DIR=./models
DEVICE=auto DEVICE=auto
HF_TOKEN: str = "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"

View File

@@ -21,7 +21,6 @@ class Settings(BaseSettings):
NSFW_THRESHOLD: float = 0.85 NSFW_THRESHOLD: float = 0.85
IMAGE_CLIP_MODEL: str = "openai/clip-vit-base-patch32" IMAGE_CLIP_MODEL: str = "openai/clip-vit-base-patch32"
CLIP_THRESHOLD: float = 0.75 CLIP_THRESHOLD: float = 0.75
HF_TOKEN: str = "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"
MODEL_CACHE_DIR: str = "./models" MODEL_CACHE_DIR: str = "./models"
DEVICE: str = "auto" DEVICE: str = "auto"

View File

@@ -1,8 +1,8 @@
from fastapi import Request from fastapi import Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.exceptions.invalid_text_exception import InvalidTextException
from app.exceptions.invalid_image_exception import InvalidImageException from app.exceptions.invalid_image_exception import InvalidImageException
from app.exceptions.invalid_text_exception import InvalidTextException
from app.models.response.error_response import ErrorResponse from app.models.response.error_response import ErrorResponse

View File

@@ -1,17 +1,13 @@
import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from app.config.logging import logger from app.config.logging import logger
from app.config.settings import settings
from app.ml.model_manager import model_manager from app.ml.model_manager import model_manager
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
if settings.HF_TOKEN:
os.environ["HF_TOKEN"] = settings.HF_TOKEN
logger.info("Loading AI models...") logger.info("Loading AI models...")
model_manager.initialize() model_manager.initialize()

View File

@@ -1,4 +1,5 @@
import torch import os
import torch
from transformers import AutoImageProcessor from transformers import AutoImageProcessor
from transformers import AutoModelForImageClassification from transformers import AutoModelForImageClassification
from transformers import AutoModelForSequenceClassification from transformers import AutoModelForSequenceClassification
@@ -17,6 +18,11 @@ class ModelManager:
self.image_model = None self.image_model = None
self.image_processor = None self.image_processor = None
def _setup_hf_token(self):
if settings.HF_TOKEN:
os.environ["HF_TOKEN"] = settings.HF_TOKEN
os.environ["HUGGING_FACE_HUB_TOKEN"] = settings.HF_TOKEN
def load_device(self): def load_device(self):
if settings.DEVICE == "auto": if settings.DEVICE == "auto":
self.device = torch.device( self.device = torch.device(

588
readme.md Normal file
View File

@@ -0,0 +1,588 @@
# AI Moderation Service
Сервис автоматической модерации пользовательского контента на базе **FastAPI** и моделей машинного обучения. Позволяет проверять тексты и изображения на наличие недопустимого контента:
- **Тексты** — токсичность (нейросетевая модель BERT) и нецензурная лексика (словарь + морфологический анализ `pymorphy3`).
- **Изображения** — NSFW-контент (нейросетевая модель `Falconsai/nsfw_image_detection`) и запрещённые категории (марихуана, наркотики, оружие, порнография, насилие) через мультимодальную модель CLIP.
Сервис предоставляет REST API и возвращает вердикт (`approved`), оценку уверенности (`score`), причину отказа (`reason`) и дополнительную диагностическую информацию.
---
## Оглавление
- [Возможности](#возможности)
- [Технологический стек](#технологический-стек)
- [Структура проекта](#структура-проекта)
- [Установка и запуск](#установка-и-запуск)
- [Конфигурация](#конфигурация)
- [API](#api)
- [POST /api/v1/moderation/text](#post-apiv1moderationtext)
- [POST /api/v1/moderation/image](#post-apiv1moderationimage)
- [Модели данных](#модели-данных)
- [Архитектура и слои](#архитектура-и-слои)
- [Логика модерации текста](#логика-модерации-текста)
- [Логика модерации изображений](#логика-модерации-изображений)
- [Обработка ошибок](#обработка-ошибок)
- [Диагностика и логирование](#диагностика-и-логирование)
- [Известные особенности](#известные-особенности)
---
## Возможности
- **Модерация текста**:
- Проверка на нецензурную лексику по словарю из ~7 350 слов (включая транслит и цифро-замены вроде «6ля»);
- Лемматизация русских слов через `pymorphy3` для поиска всех словоформ;
- Нейросетевая оценка токсичности мультиязычной моделью `textdetox/bert-multilingual-toxicity-classifier`;
- Настраиваемый порог токсичности (по умолчанию `0.90`).
- **Модерация изображений**:
- Проверка формата (JPEG/PNG/WebP) и размера файла (до 5 МБ);
- Валидация геометрии изображения (50×50 … 10000×10000 пикселей);
- Двухэтапная проверка: модель NSFW-классификации + CLIP-классификация по заранее заданным категориям;
- Комбинированный вердикт на основе нескольких моделей.
- **Готовый REST API** с автоматической документацией **Swagger UI** (`/docs`) и **ReDoc** (`/redoc`).
- **Автоматическая загрузка ML-моделей** при старте приложения (кэшируются локально).
- **Автовыбор устройства** выполнения инференса: CUDA, если доступен, иначе CPU.
---
## Технологический стек
| Компонент | Технология |
|---|---|
| Веб-фреймворк | FastAPI 0.139 |
| ASGI-сервер | Uvicorn 0.51 |
| Валидация данных | Pydantic 2.13, Pydantic-Settings |
| Работа с переменными окружения | python-dotenv |
| Нейросетевые модели | Transformers (Hugging Face), PyTorch |
| Мультимодальная модель | OpenAI CLIP (ViT-B/32) |
| Морфология русского языка | pymorphy3 |
| Обработка изображений | Pillow (PIL) |
| Язык | Python 3.12 |
> **Примечание.** В файле `ai-moderation/requirements.txt` перечислены только базовые зависимости веб-фреймворка. Зависимости машинного обучения (`transformers`, `torch`, `Pillow`, `pymorphy3` и др.) в этом файле **отсутствуют** и должны быть установлены дополнительно — см. раздел [Установка и запуск](#установка-и-запуск).
---
## Структура проекта
```
moderation-post/
├── .env # Конфигурация приложения (переменные окружения)
├── moderation-post.iml # Файл модуля PyCharm (IDEA)
├── .idea/ # Настройки IDE
├── readme.md # Данный документ
└── ai-moderation/ # Корень Python-приложения
├── requirements.txt # Базовые зависимости (без ML-пакетов)
├── models/ # Локальный кэш моделей Hugging Face
├── app/
│ ├── main.py # Точка входа: создание FastAPI-приложения
│ ├── container.py # Ручной DI-контейнер (словарь + детектор)
│ ├── lifespan.py # Жизненный цикл: загрузка моделей при старте
│ ├── config/
│ │ ├── settings.py # Настройки (pydantic-settings), чтение .env
│ │ ├── logging.py # Настройка логирования
│ │ └── image_policy.py # Список запрещённых категорий для изображений
│ ├── core/
│ │ └── dependencies.py # DI-фабрики (lru_cache) для сервисов/классификаторов
│ ├── exceptions/
│ │ ├── moderation_exception.py # Базовое исключение модерации
│ │ ├── invalid_text_exception.py # Ошибка «некорректный текст»
│ │ ├── invalid_image_exception.py # Ошибка «некорректное изображение»
│ │ └── handlers.py # Обработчики ошибок → JSON
│ ├── ml/
│ │ ├── base_classifier.py # Абстрактный базовый классификатор
│ │ ├── model_manager.py # Синглтон загрузки и хранения моделей
│ │ ├── prediction_result.py # Результат классификации текста
│ │ ├── text_classifier.py # Классификатор токсичности текста
│ │ └── image/
│ │ ├── image_classifier.py # Классификатор NSFW изображений
│ │ ├── image_prediction_result.py # Результат классификации изображений
│ │ ├── clip_classifier.py # CLIP-классификатор по категориям
│ │ └── clip_prediction_result.py # Результат CLIP-классификации
│ ├── models/
│ │ ├── dto/
│ │ │ └── text_request.py # DTO запроса модерации текста
│ │ └── response/
│ │ ├── moderation_response.py # Ответ для текста
│ │ ├── image_moderation_response.py # Ответ для изображения
│ │ └── error_response.py # Универсальный ответ об ошибке
│ ├── moderation/
│ │ ├── image/
│ │ │ └── validator.py # Проверка размеров изображения
│ │ └── profanity/
│ │ ├── dictionary.py # Загрузка словаря нецензурной лексики
│ │ ├── lemmatizer.py # Лемматизация через pymorphy3
│ │ └── detector.py # Детектор нецензурной лексики в тексте
│ ├── resources/
│ │ └── profanity_words.txt # Словарь нецензурных слов (~7 350 записей)
│ ├── routers/
│ │ ├── text_moderation_router.py # Эндпоинт POST /api/v1/moderation/text
│ │ └── image_moderation_router.py # Эндпоинт POST /api/v1/moderation/image
│ └── services/
│ ├── text_moderation_service.py # Бизнес-логика модерации текста
│ └── image_moderation_service.py # Бизнес-логика модерации изображений
└── venv/ # Виртуальное окружение Python
```
---
## Установка и запуск
### 1. Клонирование репозитория
```bash
git clone <url-репозитория>
cd moderation-post
```
### 2. Создание виртуального окружения
```bash
cd ai-moderation
python -m venv venv
# Активация (Windows)
venv\Scripts\activate
# или (Linux/macOS)
source venv/bin/activate
```
### 3. Установка зависимостей
Файл `requirements.txt` содержит только веб-зависимости. Поскольку в `requirements.txt` не входят ML-пакеты, установите зависимости полностью:
```bash
pip install -r requirements.txt
```
Дополнительно (необходимо для работы ML-части, но не перечислено в `requirements.txt`):
```bash
pip install torch
pip install transformers
pip install Pillow
pip install pymorphy3
```
> **Важно.** Для работы на GPU установите `torch` с соответствующим индексом CUDA (см. официальную документацию PyTorch), либо оставьте CPU-версию — сервис автоматически выберет доступное устройство.
### 4. Настройка окружения
Создайте файл `.env` в корне проекта (рядом с каталогом `ai-moderation`) или в самом каталоге приложения. Минимальный рабочий конфиг:
```dotenv
APP_NAME=AI Moderation Service
APP_VERSION=1.0.0
HOST=0.0.0.0
PORT=8000
DEBUG=true
# ===== AI =====
TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier
IMAGE_MODEL=Falconsai/nsfw_image_detection
IMAGE_CLIP_MODEL=openai/clip-vit-base-patch32
MODEL_CACHE_DIR=./models
DEVICE=auto
HF_TOKEN=<ваш HF-токен>
```
> **Внимание:** в файле `.env` и в `settings.py` по умолчанию уже вписаны значения `HF_TOKEN`, в том числе **реальный токен**. Рекомендуется не публиковать его и заменить на собственный. Публиковать секреты в репозиторий недопустимо.
### 5. Запуск сервера
Из каталога `ai-moderation`:
```bash
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000
```
или через FastAPI CLI:
```bash
fastapi run app/main.py --host 0.0.0.0 --port 8000
```
При старте сервис скачает указанные модели в каталог `MODEL_CACHE_DIR` (если их ещё нет) и загрузит их в память. На машине без GPU первая загрузка может занять несколько минут.
### 6. Проверка работоспособности
- Интерактивная документация API: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
---
## Конфигурация
Настройки определяются классом `Settings` в `app/config/settings.py` (pydantic-settings) и читаются из файла `.env`. Все параметры имеют значения по умолчанию.
| Переменная | Тип | По умолчанию | Описание |
|---|---|---|---|
| `APP_NAME` | str | `AI Moderation` | Название сервиса (отображается в Swagger) |
| `APP_VERSION` | str | `1.0.0` | Версия сервиса |
| `HOST` | str | `0.0.0.0` | Хост для запуска Uvicorn |
| `PORT` | int | `8000` | Порт для запуска Uvicorn |
| `DEBUG` | bool | `True` | Режим отладки |
| `TEXT_TOXIC_THRESHOLD` | float | `0.90` | Порог токсичности текста: если вероятность «токсично» ≥ порога — текст отклоняется |
| `TEXT_MODEL` | str | `textdetox/bert-multilingual-toxicity-classifier` | Модель классификации токсичности текста |
| `IMAGE_MODEL` | str | `Falconsai/nsfw_image_detection` | Модель NSFW-классификации изображений |
| `NSFW_THRESHOLD` | float | `0.85` | Порог уверенности NSFW-модели |
| `IMAGE_CLIP_MODEL` | str | `openai/clip-vit-base-patch32` | Мультимодальная модель CLIP |
| `CLIP_THRESHOLD` | float | `0.75` | Порог уверенности CLIP-классификатора |
| `HF_TOKEN` | str | — | Токен Hugging Face (подписанный/приватные модели) |
| `MODEL_CACHE_DIR` | str | `./models` | Каталог локального кэша моделей |
| `DEVICE` | str | `auto` | Устройство инференса: `auto`, `cpu`, `cuda` и т.д. |
---
## API
Все эндпоинты находятся под префиксом `/api/v1/moderation`. Сервис не содержит авторизации — эндпоинты открыты.
### POST /api/v1/moderation/text
Модерация текста. Запрос принимает JSON.
**Тело запроса** (модель `TextRequest`):
```json
{
"text": "Пример проверяемого текста"
}
```
| Поле | Тип | Ограничения |
|---|---|---|
| `text` | string | обязательное, длина от 1 до 5000 символов |
**Успешный ответ — `200 OK`** (модель `ModerationResponse`):
```json
{
"approved": true,
"score": 0.015,
"reason": "OK"
}
```
| Поле | Тип | Описание |
|---|---|---|
| `approved` | boolean | `true` — контент разрешён, `false` — отклонён |
| `score` | float | Уровень токсичности (0.0 … 1.0) |
| `reason` | string | Причина решения: `OK`, `TOXIC`, `PROFANITY` |
**Коды ошибок:**
- `400` — пустой текст / невалидная длина (`INVALID_TEXT`).
---
### POST /api/v1/moderation/image
Модерация изображения. Запрос — `multipart/form-data` с полем `file` (тип `UploadFile`).
**Ограничения загрузки:**
- Допустимые MIME-типы: `image/jpeg`, `image/png`, `image/webp`;
- Максимальный размер файла: 5 МБ;
- Допустимые размеры изображения: ширина и высота от 50 до 10000 пикселей.
**Успешный ответ — `200 OK`** (модель `ImageModerationResponse`):
```json
{
"approved": true,
"score": 1.0,
"reason": "OK",
"label": "normal",
"detected_labels": []
}
```
Пример ответа при отклонении:
```json
{
"approved": false,
"score": 0.93,
"reason": "NSFW",
"label": "nsfw",
"detected_labels": ["nsfw"]
}
```
| Поле | Тип | Описание |
|---|---|---|
| `approved` | boolean | Разрешено ли изображение |
| `score` | float | Уверенность модели, на которой принято решение |
| `reason` | string | Причина: `OK`, `NSFW`, `FORBIDDEN_CONTENT` |
| `label` | string | Наиболее вероятная метка (например, `normal`, `nsfw` или метка CLIP) |
| `detected_labels` | array of strings | Список обнаруженных запрещённых категорий |
**Коды ошибок:**
- `400` — недопустимый формат (`Unsupported image format`), битый файл (`Invalid image`), слишком маленькое/большое изображение (`INVALID_IMAGE`);
- `413` — файл больше 5 МБ (`Image too large`).
---
## Модели данных
### Запросы (DTO)
**`TextRequest`** (`app/models/dto/text_request.py`)
```python
class TextRequest(BaseModel):
text: str = Field(min_length=1, max_length=5000)
```
### Ответы
**`ModerationResponse`** (`app/models/response/moderation_response.py`) — ответ для текста:
```python
class ModerationResponse(BaseModel):
approved: bool
score: float
reason: str
```
**`ImageModerationResponse`** (`app/models/response/image_moderation_response.py`) — ответ для изображения:
```python
class ImageModerationResponse(BaseModel):
approved: bool
score: float
reason: str
label: str
detected_labels: List[str]
```
**`ErrorResponse`** (`app/models/response/error_response.py`) — универсальный ответ об ошибке:
```python
class ErrorResponse(BaseModel):
code: str
message: str
```
### Внутренние структуры (ML)
**`PredictionResult`** (`app/ml/prediction_result.py`) — результат классификации текста:
```python
@dataclass(slots=True)
class PredictionResult:
label: str
score: float
approved: bool
raw_scores: list[float]
reason: str = ""
detected_words: list[str] = field(default_factory=list)
```
**`ImagePredictionResult`** (`app/ml/image/image_prediction_result.py`) — результат классификации изображения:
```python
@dataclass(slots=True)
class ImagePredictionResult:
label: str
score: float
approved: bool
raw_scores: list[float] = field(default_factory=list)
reason: str = ""
detected_labels: list[str] = field(default_factory=list)
```
**`ClipPredictionResult`** (`app/ml/image/clip_prediction_result.py`) — результат CLIP-классификации:
```python
@dataclass(slots=True)
class ClipPredictionResult:
label: str # метка с максимальной вероятностью
score: float # вероятность метки
detected_labels: list[str] # метки выше порога CLIP_THRESHOLD
scores: dict[str, float] # все метки → вероятности
```
---
## Архитектура и слои
Проект построен по классической слоистой схеме:
```
Роутеры (routers)
↓
Сервисы (services) — бизнес-логика модерации
↓
Классификаторы (ml) + Правила (moderation)
↓
Модели Hugging Face / словарь нецензурной лексики
```
Зависимости компонентов внедряются через фабрики с декоратором `@lru_cache` (`app/core/dependencies.py`), поэтому в рамках одного процесса создаётся по одному экземпляру каждого классификатора и сервиса. Загрузка моделей выполняется один раз в `lifespan` при старте приложения.
### Точка входа — `app/main.py`
- Создаёт экземпляр `FastAPI` с названием и версией из настроек;
- Подключает роутеры модерации текста и изображений;
- Регистрирует глобальные обработчики исключений `InvalidTextException` и `InvalidImageException`;
- Жизненный цикл приложения задан в `lifespan`.
### Жизненный цикл — `app/lifespan.py`
При старте:
1. Если задан `HF_TOKEN`, он экспортируется в переменную окружения `HF_TOKEN`;
2. Вызывается `model_manager.initialize()` — загрузка устройства, токенизатора, текстовой и графической моделей.
При остановке сервис просто логирует завершение работы (освобождение GPU-памяти реализовано на уровне процесса/ОС).
### Конфигурация — `app/config/`
- **`settings.py`** — класс `Settings` (pydantic-settings) + экземпляр `settings`. Загружает `.env`, игнорирует лишние ключи (`extra="ignore"`).
- **`logging.py`** — конфигурация логирования в stdout (`logging.INFO`), логгер `ai-moderation`.
- **`image_policy.py`** — множество `FORBIDDEN_IMAGE_LABELS` с запрещёнными категориями CLIP:
- `a photo containing marijuana`
- `a photo containing drugs`
- `a photo containing weapons`
- `a pornographic photo`
- `a photo containing violence`
### DI-фабрики — `app/core/dependencies.py`
Использует `@lru_cache`, что превращает фабрики в синглтоны:
| Фабрика | Возвращает |
|---|---|
| `get_text_classifier` | `TextClassifier` |
| `get_clip_classifier` | `ClipClassifier` |
| `get_image_classifier` | `ImageClassifier` |
| `get_profanity_detector` | `ProfanityDetector` (словарь + лемматизатор) |
| `get_text_moderation_service` | `TextModerationService` |
| `get_image_moderation_service` | `ImageModerationService` (классификатор + валидатор + CLIP) |
| `get_image_validator` | `ImageValidator` |
> Файл `app/container.py` содержит аналогичный, но неиспользуемый «ручной» контейнер, создающий детектор нецензурной лексики. Основной путь внедрения зависимостей — через `core/dependencies.py`.
### Слой ML — `app/ml/`
- **`base_classifier.py`** — абстрактный класс `BaseClassifier` с методом `predict(value)`;
- **`model_manager.py`** — синглтон `model_manager`, который:
- выбирает устройство (`auto` → CUDA при доступности, иначе CPU);
- загружает токенизатор и текстовую модель (`AutoTokenizer`, `AutoModelForSequenceClassification`);
- загружает процессор и модель изображений (`AutoImageProcessor`, `AutoModelForImageClassification`);
- переводит модели в режим `eval()`;
- **`text_classifier.py`** — `TextClassifier.predict(text)`:
- токенизирует текст (макс. 512 токенов, `truncation`, `padding`);
- прогоняет через модель без градиентов (`torch.no_grad`);
- применяет `softmax` к логитам;
- возвращает `PredictionResult` со `score = вероятность класса 1` (токсичный);
- **`image/image_classifier.py`** — `ImageClassifier.predict(image)`:
- обрабатывает изображение процессором модели;
- возвращает `ImagePredictionResult` с меткой из `id2label` (обычно `nsfw`/`normal`);
- **`image/clip_classifier.py`** — `ClipClassifier`:
- загружает CLIP-модель и процессор сразу в конструкторе (при первом обращении);
- задаёт список из 6 текстовых меток (`a normal photo` + 5 запрещённых);
- считает вероятности соответствия изображения каждой метке;
- собирает метки, превышающие `CLIP_THRESHOLD`, и возвращает `ClipPredictionResult`.
### Слой правил — `app/moderation/`
**Нецензурная лексика (`profanity/`):**
- **`dictionary.py`** — `ProfanityDictionary` загружает словарь из текстового файла в множество (`set`), убирает BOM и пустые строки, приводит к нижнему регистру. Метод `contains(word)` проверяет точное вхождение.
- **`lemmatizer.py`** — `Lemmatizer` на базе `pymorphy3.MorphAnalyzer`, метод `normalize(word)` возвращает нормальную форму (лемму) слова.
- **`detector.py`** — `ProfanityDetector.detect(text)`:
1. регулярным выражением `[а-яА-ЯёЁ]+` извлекает русскоязычные слова (после приведения к нижнему регистру);
2. каждое слово лемматизирует;
3. проверяет, начинается ли лемма с какого-либо слова из словаря (префиксное сравнение через `startswith`);
4. возвращает список найденных слов в исходном виде.
**Валидация изображений (`image/validator.py`):**
`ImageValidator.validate(image)` проверяет размеры изображения и выбрасывает `InvalidImageException` с описанием проблемы, если:
- ширина/высота меньше `MIN_WIDTH/MIN_HEIGHT` (50);
- ширина/высота больше `MAX_WIDTH/MAX_HEIGHT` (10000).
---
## Логика модерации текста
`TextModerationService.moderate(text)` (`app/services/text_moderation_service.py`):
1. **Проверка входа.** Если текст `None` или состоит из пробелов — выбрасывается `InvalidTextException("Text is empty")` → HTTP 400.
2. **Детектор нецензурной лексики.** `profanity_detector.detect(text)`:
- если найдены запрещённые слова, сразу возвращается `PredictionResult(label="PROFANITY", score=1.0, approved=False, reason="PROFANITY", detected_words=[...])` — нейросеть не вызывается (экономия ресурсов);
3. **Нейросетевая оценка.** Иначе вызывается `TextClassifier.predict(text)`:
- `approved = score < TEXT_TOXIC_THRESHOLD`;
- `reason = "OK"` при одобрении, иначе `"TOXIC"`.
Итоговое решение передаётся в роутер и маппится в `ModerationResponse`.
---
## Логика модерации изображений
`ImageModerationService.moderate(image)` (`app/services/image_moderation_service.py`):
1. **Валидация.** `ImageValidator.validate(image)` — проверка размеров (см. выше).
2. **Шаг 1 — NSFW-модель.** `ImageClassifier.predict(image)`:
- если метка `nsfw` и уверенность ≥ `NSFW_THRESHOLD` (0.85) → решение **отклонить**: `reason="NSFW"`, `detected_labels=["nsfw"]`;
3. **Шаг 2 — CLIP-модель.** `ClipClassifier.predict(image)`:
- из меток, превысивших `CLIP_THRESHOLD`, отбираются только те, что входят в `FORBIDDEN_IMAGE_LABELS`;
- если такие категории найдены → решение **отклонить**: `reason="FORBIDDEN_CONTENT"`, `detected_labels=[...]`;
4. **Шаг 3 — «нормальное» изображение.** Если ни одно из правил не сработало → `approved=True`, `reason="OK"`, `label="normal"`, `score=1.0`.
Таким образом, изображение одобряется только если оно прошло и NSFW-модель, и CLIP-проверку по запрещённым категориям.
---
## Обработка ошибок
| Класс | Наследует | Назначение |
|---|---|---|
| `ModerationException` | `Exception` | Базовый класс, хранит поле `message` |
| `InvalidTextException` | `ModerationException` | Некорректный текст (пустой) |
| `InvalidImageException` | `ModerationException` | Некорректное изображение (размеры) |
Обработчики в `app/exceptions/handlers.py` преобразуют исключения в HTTP-ответ:
| Исключение | HTTP-код | `code` в теле |
|---|---|---|
| `InvalidTextException` | 400 | `INVALID_TEXT` |
| `InvalidImageException` | 400 | `INVALID_IMAGE` |
Тело ошибки всегда в формате `ErrorResponse`:
```json
{
"code": "INVALID_IMAGE",
"message": "Image width is too small: 10"
}
```
Ошибки загрузки файла (формат/размер) обрабатываются в роутере напрямую через `HTTPException` с соответствующими кодами (`400`, `413`).
---
## Диагностика и логирование
- Логи пишутся в stdout с уровнем `INFO` в формате `время уровень сообщение`.
- При старте логируются шаги загрузки моделей и выбранное устройство.
- В классификаторах присутствуют отладочные `print`-вызовы:
- `TextClassifier` печатает входные тензоры, логиты и вероятности;
- `ImageClassifier` печатает метки, предсказание и вероятности;
- `ClipClassifier` печатает выбранное устройство при инициализации.
---
## Известные особенности
1. **Неполный `requirements.txt`.** В файл не включены ML-зависимости (`torch`, `transformers`, `Pillow`, `pymorphy3`), хотя они обязательны для работы. Для воспроизводимой установки их следует добавить в `requirements.txt`.
2. **`HF_TOKEN` зашит по умолчанию.** Токен присутствует и в `.env`, и как значение по умолчанию в `settings.py`. Его необходимо заменить/вычистить перед публикацией проекта.
3. **Отладочный вывод.** Классификаторы содержат `print()`-вызовы; в «боевом» режиме их стоит заменить на логирование через `logger`.
4. **Дублирование DI.** Логика внедрения зависимостей дублируется в `app/container.py` (не используется) и в `app/core/dependencies.py` (основной путь).
5. **Производительность.** Модели загружаются в память целиком; на CPU инференс CLIP-модели может быть медленным. Загрузка моделей происходит при старте приложения (см. `lifespan`), первое обращение к CLIP-эндпоинту также выполняет инициализацию модели.