# AI Moderation Service Сервис автоматической модерации пользовательского контента на базе **FastAPI** и моделей машинного обучения. Позволяет проверять тексты и изображения на наличие недопустимого контента: - **Тексты** — токсичность (нейросетевая модель BERT) и нецензурная лексика (словарь + морфологический анализ `pymorphy3`). - **Изображения** — NSFW-контент (нейросетевая модель `Falconsai/nsfw_image_detection`), оружие (zero-shot детектор **OWLv2**) и запрещённые категории (марихуана, наркотики, порнография, насилие) через мультимодальную модель 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-классификация по запрещённым категориям (наркотики, насилие); - Отдельный детектор оружия на базе **OWLv2** (zero-shot object detection) — замена ненадёжной CLIP-проверке категории `weapons`; - Комбинированный вердикт на основе нескольких моделей. - **Готовый 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) | | Детектор оружия | OWLv2 (`google/owlv2-base-patch16-ensemble`) | | Морфология русского языка | 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-классификации │ │ ├── weapon_detector.py # Детектор оружия (OWLv2) │ │ └── weapon_prediction_result.py # Результат детекции оружия │ ├── 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 WEAPON_MODEL=google/owlv2-base-patch16-ensemble 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-классификатора | | `WEAPON_MODEL` | str | `google/owlv2-base-patch16-ensemble` | Модель zero-shot детекции оружия (OWLv2) | | `WEAPON_THRESHOLD` | float | `0.40` | Порог уверенности детектора оружия | | `WEAPON_MIN_AREA_FRACTION` | float | `0.005` | Минимальная доля площади изображения для бокса (0.5%), отсекает мелкие ложные срабатывания | | `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 violence` Порнография и оружие в этот список не входят: первая обрабатывается NSFW-моделью (шаг 1), второе — отдельным детектором на базе OWLv2 (шаг 2). ### DI-фабрики — `app/core/dependencies.py` Использует `@lru_cache`, что превращает фабрики в синглтоны: | Фабрика | Возвращает | |---|---| | `get_text_classifier` | `TextClassifier` | | `get_clip_classifier` | `ClipClassifier` | | `get_image_classifier` | `ImageClassifier` | | `get_weapon_detector` | `WeaponDetector` (OWLv2) | | `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-модель и процессор сразу в конструкторе (при первом обращении); - задаёт список из 4 текстовых меток (`a normal photo` + 3 запрещённые: наркотики, насилие). Порнография отдельно обрабатывается NSFW-моделью на шаге 1; - считает вероятности соответствия изображения каждой метке; - собирает метки, превышающие `CLIP_THRESHOLD`, и возвращает `ClipPredictionResult`; - **`image/weapon_detector.py`** — `WeaponDetector`: - загружает OWLv2-модель и процессор (zero-shot object detection) при первом обращении; - ищет по 15 текстовым запросам: `gun`, `pistol`, `rifle`, `shotgun`, `knife`, `machete`, `sword`, `bomb`, `grenade` и др.; - через `post_process_object_detection` получает боксы и уверенность по каждой запрошенной категории; - отсеивает боксы меньше `WEAPON_MIN_AREA_FRACTION` от площади изображения и с уверенностью ниже `WEAPON_THRESHOLD`; - возвращает `WeaponPredictionResult` с найденными категориями. ### Слой правил — `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 — детектор оружия.** `WeaponDetector.predict(image)`: - zero-shot детекция по запросам оружия (OWLv2, боксы + уверенность); - если найдена категория с уверенностью ≥ `WEAPON_THRESHOLD` и боксом больше `WEAPON_MIN_AREA_FRACTION` → решение **отклонить**: `reason="FORBIDDEN_CONTENT"`, `detected_labels=[...]`; 4. **Шаг 3 — CLIP-модель.** `ClipClassifier.predict(image)`: - из меток, превысивших `CLIP_THRESHOLD`, отбираются только те, что входят в `FORBIDDEN_IMAGE_LABELS` (наркотики, насилие); - если такие категории найдены → решение **отклонить**: `reason="FORBIDDEN_CONTENT"`, `detected_labels=[...]`; 5. **Шаг 4 — «нормальное» изображение.** Если ни одно из правил не сработало → `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-эндпоинту также выполняет инициализацию модели.