fixed code v2

This commit is contained in:
SlimusMinus
2026-08-06 23:45:38 +03:00
parent fdf2e2a8d5
commit 4c32c5f4dd
7 changed files with 598 additions and 7 deletions

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-эндпоинту также выполняет инициализацию модели.