From 4c32c5f4ddbd923a922089a30a87d84098e97b19 Mon Sep 17 00:00:00 2001 From: SlimusMinus Date: Thu, 6 Aug 2026 23:45:38 +0300 Subject: [PATCH] fixed code v2 --- .env | 2 + ai-moderation/app/config/settings.py | 1 - ai-moderation/app/exceptions/handlers.py | 2 +- ai-moderation/app/lifespan.py | 4 - ai-moderation/app/ml/model_manager.py | 8 +- .../apptests/test_text_classifier.py | 0 readme.md | 588 ++++++++++++++++++ 7 files changed, 598 insertions(+), 7 deletions(-) delete mode 100644 ai-moderation/apptests/test_text_classifier.py create mode 100644 readme.md diff --git a/.env b/.env index 7beb4a7..5771afd 100644 --- a/.env +++ b/.env @@ -15,3 +15,5 @@ TEXT_MODEL=textdetox/bert-multilingual-toxicity-classifier MODEL_CACHE_DIR=./models DEVICE=auto + +HF_TOKEN: str = "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" \ No newline at end of file diff --git a/ai-moderation/app/config/settings.py b/ai-moderation/app/config/settings.py index 5e35fd2..977f504 100644 --- a/ai-moderation/app/config/settings.py +++ b/ai-moderation/app/config/settings.py @@ -21,7 +21,6 @@ class Settings(BaseSettings): NSFW_THRESHOLD: float = 0.85 IMAGE_CLIP_MODEL: str = "openai/clip-vit-base-patch32" CLIP_THRESHOLD: float = 0.75 - HF_TOKEN: str = "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" MODEL_CACHE_DIR: str = "./models" DEVICE: str = "auto" diff --git a/ai-moderation/app/exceptions/handlers.py b/ai-moderation/app/exceptions/handlers.py index fe84e8c..597a27c 100644 --- a/ai-moderation/app/exceptions/handlers.py +++ b/ai-moderation/app/exceptions/handlers.py @@ -1,8 +1,8 @@ from fastapi import Request 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_text_exception import InvalidTextException from app.models.response.error_response import ErrorResponse diff --git a/ai-moderation/app/lifespan.py b/ai-moderation/app/lifespan.py index 82ba93a..d16a0b2 100644 --- a/ai-moderation/app/lifespan.py +++ b/ai-moderation/app/lifespan.py @@ -1,17 +1,13 @@ -import os from contextlib import asynccontextmanager from fastapi import FastAPI from app.config.logging import logger -from app.config.settings import settings from app.ml.model_manager import model_manager @asynccontextmanager async def lifespan(app: FastAPI): - if settings.HF_TOKEN: - os.environ["HF_TOKEN"] = settings.HF_TOKEN logger.info("Loading AI models...") model_manager.initialize() diff --git a/ai-moderation/app/ml/model_manager.py b/ai-moderation/app/ml/model_manager.py index 08cb456..6a7f1dc 100644 --- a/ai-moderation/app/ml/model_manager.py +++ b/ai-moderation/app/ml/model_manager.py @@ -1,4 +1,5 @@ -import torch +import os +import torch from transformers import AutoImageProcessor from transformers import AutoModelForImageClassification from transformers import AutoModelForSequenceClassification @@ -17,6 +18,11 @@ class ModelManager: self.image_model = 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): if settings.DEVICE == "auto": self.device = torch.device( diff --git a/ai-moderation/apptests/test_text_classifier.py b/ai-moderation/apptests/test_text_classifier.py deleted file mode 100644 index e69de29..0000000 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..39ad86f --- /dev/null +++ b/readme.md @@ -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 +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-эндпоинту также выполняет инициализацию модели.