# Контекст проекта YOLO Train WebUI Дата актуализации: 2026-07-18 Этот файл — основной технический контекст проекта для разработчиков и агентов. История найденных и исправленных дефектов находится в `PROJECT_ISSUES.md`. ## 1. Назначение и границы проекта YOLO Train WebUI — локальное веб-приложение для настройки и запуска обучения Ultralytics YOLO. Оно предоставляет форму конфигурации, профили запусков, live-логи, прогресс по эпохам, графики метрик, мягкую остановку и интеграцию с MLflow. Поддерживаемые задачи: - `detect` — детекция объектов; - `segment` — сегментация; - `classify` — классификация; - `pose` — оценка поз; - `obb` — ориентированные bounding boxes. Приложение рассчитано на локального доверенного пользователя и один активный запуск обучения. Это не многопользовательская платформа, не планировщик задач и не сервис хранения датасетов. В нём нет встроенных учётных записей, ролей или аутентификации. ## 2. Технологии | Область | Технология | |---|---| | Backend/API | Python 3.11+, FastAPI, Uvicorn | | Обучение | Ultralytics YOLO, PyTorch | | Эксперименты | MLflow | | Frontend | HTML, CSS, vanilla JavaScript | | Графики | Chart.js из CDN | | Real-time | WebSocket | | Зависимости | `uv`, frozen-набор в `uv.lock` | | Упаковка | Hatchling | | Тесты | pytest, FastAPI TestClient/httpx, Node.js smoke-test | | Контейнер | Docker, Docker Compose | Основные зависимости объявлены в `pyproject.toml`: `fastapi`, `uvicorn`, `websockets`, `ultralytics`, `mlflow`. Dev-группа содержит `pytest` и `httpx`. Python package называется `yolo-train-webui`, текущая версия — `0.1.0`; wheel собирается Hatchling только из `src/yolo_webui`. `yolo_webui.__init__` публично экспортирует `TrainingConfig`, `TrainingEvent` и `TrainingRunner`. ## 3. Структура репозитория ```text . ├── .agents/ │ ├── PROJECT_CONTEXT.md # этот технический контекст │ └── PROJECT_ISSUES.md # аудит и история исправлений ├── src/yolo_webui/ │ ├── __init__.py # публичные Python-экспорты │ ├── __main__.py # запуск `python -m yolo_webui` │ ├── app.py # FastAPI, TrainingManager, REST и WebSocket │ ├── config.py # dataclass-конфигурации и валидация │ ├── dataset_splitter.py # detection-style train/val splitter │ ├── subprocess_runner.py # дочерний процесс обучения и stdout-протокол │ ├── trainer.py # Ultralytics callbacks, MLflow, cancellation │ └── static/ │ ├── index.html # форма и панель мониторинга │ ├── app.js # browser state, API, WebSocket, Chart.js │ └── style.css # всё визуальное оформление ├── tests/ │ ├── test_app.py # API и TrainingManager │ ├── test_config.py # конфигурация, безопасность, MLflow env │ ├── test_splitter.py # классы и разбиение датасета │ ├── test_subprocess_runner.py │ ├── test_trainer.py # callbacks и остановка │ ├── test_frontend.py # запуск Node-проверок из pytest │ └── frontend_smoke.js # browser stubs, форма и графики ├── Dockerfile ├── docker-compose.yml ├── pyproject.toml ├── uv.lock └── README.md ``` Рабочие каталоги не входят в Git: - `datasets/` — локальные датасеты; - `models/` — локальные веса и YAML моделей; - `runs/` — результаты Ultralytics и JSON-профили; - `.yolo-webui/` — сгенерированные split-файлы внутри датасетов; - `mlflow.db`, `mlruns/`, `mlflow/` — локальные данные MLflow; - `.venv/`, кэши Python и pytest. ## 4. Архитектура во время выполнения ```text Browser ├── HTTP JSON ───────────────┐ └── WebSocket /api/ws ───────┤ v FastAPI / TrainingManager (основной процесс Uvicorn) ├── хранит LiveState и WebSocket-клиентов ├── сохраняет профили в runs/sessions └── запускает background thread | v Python subprocess: yolo_webui.subprocess_runner ├── читает временный JSON config ├── ставит SIGTERM/SIGINT handlers ├── создаёт TrainingRunner ├── запускает Ultralytics YOLO.train() └── пишет события, логи и результат в stdout | ├── dataset / generated split ├── models / official model download ├── runs / training artifacts └── MLflow storage ``` Изоляция обучения в subprocess нужна, чтобы тяжёлый Ultralytics/PyTorch не блокировал ASGI event loop, stdout можно было транслировать в браузер, а зависший запуск — принудительно завершить. Важная деталь: `TrainingRunner` используется в двух процессах. - В родительском `TrainingManager` он хранит ссылку на subprocess и управляет сигналами остановки. - В дочернем процессе отдельный экземпляр владеет моделью Ultralytics и выставляет `trainer.stop = True`. ## 5. Точки входа и запуск CLI entry point из `pyproject.toml`: ```bash uv run yolo-train-webui ``` Альтернативный модульный запуск: ```bash uv run -m yolo_webui ``` Оба варианта вызывают `yolo_webui.app:main`. CLI принимает: - `--host`, default `127.0.0.1`; - `--port`, default `8000`. Перед запуском Uvicorn устанавливается `MPLBACKEND=Agg`. В Docker приложение слушает `0.0.0.0:8000` внутри контейнера, но Compose публикует его только как `127.0.0.1:8000` на host. ## 6. Backend и состояние обучения ### `LiveState` Глобальный `TrainingManager` хранит единственное состояние: - `status`; - текущую и общую эпохи; - список логов; - историю метрик; - `output_dir`; - `stop_requested`; - `last_event_kind` для классификации результата. Состояния: ```text idle └── start -> preparing ├── event started -> training │ ├── normal exit 0 -> succeeded │ ├── stop -> stopping -> cancelled │ └── error -> failed ├── stop -> stopping -> cancelled/failed └── setup error -> failed ``` `finished` больше не создаётся backend-ом; frontend понимает его только для совместимости со старым состоянием. Новый запуск разрешён лишь когда нет активного статуса и предыдущий background thread уже завершён. ### Потоки и lock `TrainingManager._lock` защищает `LiveState`, ссылку на thread и набор WebSocket. Нельзя выполнять `broadcast()` внутри `with self._lock`: broadcast сам читает защищённые данные, и повторный захват обычного `threading.Lock` вызовет deadlock. WebSocket привязывается к event loop Uvicorn при подключении. Вызовы broadcast из фонового потока передаются через `asyncio.run_coroutine_threadsafe()`. Отправки сериализуются `asyncio.Lock`; failed-клиенты логируются и удаляются. ### Запуск subprocess `TrainingManager._run_subprocess()`: 1. сериализует `TrainingConfig.to_dict()` во временный JSON; 2. запускает текущий interpreter с `-u -m yolo_webui.subprocess_runner`; 3. объединяет stderr со stdout; 4. читает поток посимвольно, различая `\r` и `\n` для progress-строк; 5. обновляет состояние и транслирует события; 6. ждёт return code, удаляет временный JSON и очищает ссылку на процесс. ### Внутренний stdout-протокол Дочерний процесс печатает специальные маркеры: ```text __YOLO_WEBUI_READY__ __YOLO_WEBUI_EVENT__:{"kind":"epoch","message":"...","epoch":1,"total_epochs":100} __YOLO_WEBUI_RESULT__:/absolute/path/to/run ``` - `READY` означает, что signal handlers уже установлены и отложенный stop можно безопасно доставить. - `EVENT` несёт `kind`, `message`, `epoch`, `total_epochs`. - `RESULT` передаёт каталог результатов. - Любая другая строка считается обычным логом. События от `TrainingRunner`: `info`, `started`, `epoch`, `success`, `cancelled`, `warning`. Исключение выводится traceback-ом в stderr/stdout и даёт return code `1`. ### Классификация завершения Backend различает: - `succeeded` — return code `0` без подтверждённой остановки; - `cancelled` — был stop и subprocess завершился с `0`, прислал `cancelled` или был убит force-stop таймером; - `failed` — ненулевой код без подтверждённой отмены, в том числе реальная ошибка, случившаяся после нажатия Stop. ## 7. Остановка обучения Остановка кооперативная и двухуровневая: 1. `POST /api/train/stop` ставит `stop_requested` и статус `stopping`. 2. Если subprocess ещё не прислал `READY`, запрос сохраняется. 3. После `READY` родитель отправляет `SIGTERM`. 4. Signal handler дочернего процесса вызывает `TrainingRunner.request_stop()`. 5. Runner выставляет `trainer.stop = True` сразу либо в ближайшем callback. 6. Ultralytics штатно завершает callbacks и сохранение результатов. 7. Если subprocess не завершился за 30 секунд, parent вызывает `kill()`. `prepare_run()` перед каждым новым запуском очищает stop-флаги и старый таймер. ## 8. REST и WebSocket API | Метод | Путь | Назначение | |---|---|---| | GET | `/` | Возвращает `static/index.html` | | GET | `/static/*` | CSS и JavaScript | | GET | `/api/config/defaults` | Полный default `TrainingConfig` | | GET | `/api/datasets` | Верхнеуровневые каталоги и YAML из `datasets/` | | GET | `/api/models` | Верхнеуровневые `.pt/.pth/.yaml/.yml` из `models/` | | GET | `/api/sessions` | Список профилей без `last_run` | | GET | `/api/sessions/{name}` | Загрузить профиль; `last_run` читать можно | | POST | `/api/sessions/{name}` | Сохранить произвольный JSON профиля | | DELETE | `/api/sessions/{name}` | Удалить профиль | | GET | `/api/train/status` | Текущее состояние, эпохи, результат, метрики, логи | | POST | `/api/train/start` | Провалидировать config, сохранить `last_run`, запустить | | POST | `/api/train/stop` | Запросить остановку | | WS | `/api/ws` | Init-снимок и live-события | FastAPI также оставляет включёнными стандартные OpenAPI endpoints: `/docs`, `/redoc`, `/openapi.json`. WebSocket server → browser сообщения: - `init`: полный snapshot состояния, логов и метрик при подключении; - `status`: новое состояние и опциональный `output_dir`; - `log`: `message` и `level`; - `progress`: эпоха, total, извлечённые метрики и сообщение. Browser → server сообщения не используются; endpoint только читает и отбрасывает их, поддерживая соединение открытым. Профили хранятся в `runs/sessions/{name}.json`. Имя: 1–64 символа из латинских букв, цифр, `_`, `-`. `last_run` зарезервирован для автосохранения при старте: его можно прочитать, но нельзя создать или удалить через profile endpoints. ## 9. Конфигурация обучения ### `TrainingConfig` | Поле | Default | Передача в Ultralytics | |---|---:|---| | `dataset` | обязательно; API default `coco8.yaml` | `data` | | `model` | обязательно; API default `yolo11n.pt` | аргумент конструктора `YOLO()` | | `task` | `detect` | аргумент конструктора `YOLO()` | | `epochs` | `100` | `epochs` | | `image_size` | `640` | `imgsz` | | `batch_size` | `16` | `batch` | | `device` | пусто | `device`, только если задано | | `workers` | `8` | `workers`; `0` допустим | | `patience` | `100` | `patience`; `0` допустим | | `project` | `runs/train` | `project` | | `run_name` | пусто | `name`, только если задано | | `augmentation` | включена | набор augmentation kwargs | | `mlflow` | включён | Ultralytics setting и env | | `split` | выключен | preprocessing до `YOLO.train()` | Основная валидация: - `epochs >= 1`, `image_size >= 32`; - batch положительный или `-1`; `0` и значения `< -1` запрещены; - `workers >= 0`, `patience >= 0`; - задача входит в фиксированный список; - detection-style auto split запрещён для `classify`; - dataset/model/project проходят security path validation. Модель без `/` или `\` считается именем и резолвится как `models/{name}`. ### `DatasetSplitConfig` - `enabled=False`; - `train_ratio=0.8`, допустимо `0.1…0.95`; - `classes_path=""`, пустое значение включает автопоиск. ### `MlflowConfig` - `enabled=True`; - `tracking_uri="sqlite:///mlflow.db"`; - `experiment_name="yolo-webui"`; - `run_name=""`. Если MLflow включён, tracking URI и experiment name не могут быть пустыми. `mlflow_environment()` временно выставляет: - `MLFLOW_TRACKING_URI`; - `MLFLOW_EXPERIMENT_NAME`; - `MLFLOW_RUN`; - `MLFLOW_KEEP_RUN_ACTIVE=False`. После обучения предыдущие значения окружения восстанавливаются. В Ultralytics глобальная настройка `mlflow` включается/выключается через `settings.update()`. ### `AugmentationConfig` Default-параметры: ```text hsv_h=0.015 hsv_s=0.7 hsv_v=0.4 degrees=0.0 translate=0.1 scale=0.5 shear=0.0 perspective=0.0 flipud=0.0 fliplr=0.5 bgr=0.0 mosaic=1.0 mixup=0.0 cutmix=0.0 copy_paste=0.0 erasing=0.4 close_mosaic=10 copy_paste_mode=flip auto_augment=randaugment ``` Вероятности и доли валидируются в диапазоне `0…1`; `degrees`, `shear` и `close_mosaic` не могут быть отрицательными. Режимы copy-paste: `flip`, `mixup`. Политики AutoAugment: `randaugment`, `autoaugment`, `augmix`. Если augmentation выключена, эти kwargs вообще не передаются в Ultralytics. ## 10. Работа с датасетами Auto split предназначен только для detection-style структуры: ```text dataset/ ├── images/ │ └── **/*.{jpg,jpeg,png,bmp,webp,tif,tiff} └── labels/ └── **/*.txt ``` Изображения и labels могут быть вложенными. Для split нужно минимум два изображения. Shuffle детерминирован seed-ом `42`; train и val всегда получают минимум по одному изображению. Порядок определения классов: 1. явно заданный `classes_path` — авторитетный, без fallback при ошибке; 2. корневой `classes.txt`; 3. `labels/classes.txt`; 4. первый по имени корневой `.yaml/.yml` с полем `names`; 5. вывод диапазона `0…max_id` из всех label-файлов с именами `class_N`. Поддерживаются text, YAML list и YAML dict. ID должны быть целыми, неповторяющимися и последовательными от `0`; пустые имена запрещены. Каждый split создаётся эксклюзивно: ```text dataset/.yolo-webui/splits/{uuid}/ ├── train.txt # абсолютные пути изображений ├── val.txt # абсолютные пути изображений └── dataset.yaml ``` Итоговый YAML сохраняет дополнительные ключи исходного YAML, например `kpt_shape` и `flip_idx`, но перезаписывает `path`, `train`, `val` и `names`. Проверенный результат `read_classes()` всегда авторитетен. Для `classify` auto split отключён: пользователь должен предоставить готовую структуру `train/`, `val/` или `test/` с подкаталогами классов. ## 11. TrainingRunner и метрики Перед обучением Runner: 1. повторно валидирует config; 2. при необходимости создаёт split и заменяет `data` на generated YAML; 3. импортирует Ultralytics; 4. включает/выключает MLflow integration; 5. создаёт `YOLO(config.resolved_model, task=config.task)`; 6. подключает callbacks `on_train_start`, `on_train_epoch_end`, `on_train_end`; 7. вызывает `model.train(**config.train_kwargs())`. Epoch callback берёт numeric metrics из `trainer.metrics`, форматирует максимум три первых значения и отправляет их в текстовом сообщении. Parent разбирает пары `key=value`, поэтому live chart сейчас показывает не более трёх метрик на эпоху. Если `trainer.save_dir` существует, его путь передаётся parent-у как результат. Restricted checkpoint loading принудительно включён: ```text ULTRALYTICS_SAFE_LOAD=1 ``` ## 12. Frontend Frontend не имеет сборщика и framework: `index.html`, `style.css` и `app.js` отдаются FastAPI как статические файлы. Chart.js загружается с jsDelivr CDN. Левая панель содержит профили и вкладки: - «Основное» — task, model, dataset, auto split; - «Обучение» — epochs, image size, batch, device, workers, patience, output; - «Аугментация» — все поля `AugmentationConfig`; - «MLflow» — enabled, tracking URI, experiment и run name. Правая панель содержит статус, timer, progress bar, Start/Stop, live chart и журнал. Browser state: - активная вкладка хранится в `localStorage.active_tab`; - выбранный профиль — `localStorage.selected_profile`; - черновик формы — `localStorage.draft_config`; - при старте загрузки приоритет: draft → `last_run` → API defaults; - список датасетов и моделей запрашивается у backend; - стандартные модели YOLO11 выбираются динамически по task; - при disconnect WebSocket переподключается через 5 секунд; - `init` восстанавливает status, логи, progress и историю графика. Числа читаются через `Number.parseInt/parseFloat` и проверку `Number.isNaN`. Нельзя заменять это на `value || default`: допустимые `0` для workers, patience, close_mosaic и augmentation-параметров должны сохраняться. Chart datasets создаются по фактически пришедшим ключам. Новая метрика может появиться на поздней эпохе; пропущенные точки заполняются `null`, чтобы серии не сдвигались относительно labels. ## 13. Безопасность и доверенная модель Приложение не имеет аутентификации. Безопасность по умолчанию строится на локальной публикации и ограничении файловых путей. Default доверенные корни: | Назначение | Корни | |---|---| | Dataset и classes | `./datasets` | | Model/checkpoint | `./models`, `./runs` | | Training output | `./runs` | Дополнительные корни перечисляются через системный `os.pathsep`: - `YOLO_WEBUI_DATA_ROOTS`; - `YOLO_WEBUI_MODEL_ROOTS`; - `YOLO_WEBUI_RUN_ROOTS`. Проверка запрещает URL, нормализует путь через `resolve(strict=False)` и проверяет принадлежность корню, включая существующие symlink-компоненты. Безопасные bare identifiers разрешены для официальных имён, но существующий одноимённый файл вне доверенного root отвергается. Model-файлы ограничены расширениями `.pt`, `.pth`, `.yaml`, `.yml`. Compose публикует только `127.0.0.1:8000:8000`. Для доступа из сети обязателен аутентифицирующий reverse proxy и явная оценка риска: API может запускать тяжёлое обучение, останавливать его и управлять профилями. ## 14. Docker Dockerfile: - основан на `python:3.11-slim`; - устанавливает системные библиотеки для OpenCV/PyTorch/Ultralytics; - фиксирует `uv==0.10.6`; - копирует `pyproject.toml`, `uv.lock`, README; - выполняет `uv sync --locked --no-dev` в `/opt/venv`; - включает `ULTRALYTICS_SAFE_LOAD=1`; - запускает `yolo-train-webui --host 0.0.0.0 --port 8000`. Compose монтирует: ```text ./datasets -> /workspace/datasets ./runs -> /workspace/runs ./models -> /workspace/models ./models/.config -> /root/.config/Ultralytics ``` Порт 8000 опубликован только на loopback. Порт 5000 объявлен в image, но Compose не запускает и не публикует MLflow UI. GPU reservation оставлена как закомментированный пример для NVIDIA/Linux. ## 15. Тесты и проверки Текущий regression suite содержит 49 pytest-тестов. - `test_app.py`: defaults/status, profiles, deadlock, background WebSocket loop, финальные состояния. - `test_config.py`: kwargs, validation, zero-compatible параметры, MLflow env, security roots и URL. - `test_splitter.py`: форматы классов, приоритеты, nested data, уникальные outputs, сохранение YAML metadata. - `test_subprocess_runner.py`: return codes, READY/RESULT и traceback. - `test_trainer.py`: Ultralytics callbacks, metrics, ранний stop, сигналы. - `test_frontend.py` + `frontend_smoke.js`: syntax, сохранение нулей, динамические chart series в fake browser environment. Основные команды: ```bash uv sync --locked uv run pytest -q uv run python -m compileall -q src tests node --check src/yolo_webui/static/app.js node tests/frontend_smoke.js uv lock --check docker compose config git diff --check ``` Python-команды проекта следует выполнять через `uv run`, чтобы использовать зафиксированное окружение. ## 16. Согласованное изменение проекта При добавлении или переименовании config-поля обычно нужно изменить вместе: 1. dataclass, default, validation и `train_kwargs()` в `config.py`; 2. `TrainingConfig.from_dict()`; 3. поле в `static/index.html`; 4. чтение в `getFormConfig()` и восстановление в `applyConfig()` в `app.js`; 5. backend/frontend regression tests; 6. README и этот контекст, если меняется пользовательский контракт. При добавлении нового состояния обучения нужно обновить: 1. backend state machine и финальную классификацию; 2. WebSocket status payload; 3. `updateUIStatus()`; 4. CSS-селекторы `status-*`; 5. тесты переходов и reconnect snapshot. При изменении subprocess-протокола синхронно меняются `subprocess_runner.py` и parser в `TrainingManager._handle_subprocess_line()`. Префиксы протокола нельзя печатать в обычных логах. Критические инварианты: - не вызывать WebSocket send из нового или чужого event loop; - не вызывать `broadcast()` под `TrainingManager._lock`; - не объединять `succeeded`, `cancelled`, `failed` в общий `finished`; - не использовать JS truthiness для числовых полей; - явно указанный classes-файл всегда авторитетен; - не создавать split поверх пользовательских файлов; - не снимать `--locked` с Docker/CI установки; - не расширять сетевую публикацию без аутентификации; - сохранять traceback и ошибки доставки в наблюдаемых логах. ## 17. Текущие ограничения - Только один активный training job и один глобальный in-memory `LiveState`. - После перезапуска server live state теряется; сохраняются лишь JSON-профили, `last_run`, training artifacts и MLflow data. - Нет очереди, scheduler, истории runs API, upload API и файлового браузера. - Нет встроенной аутентификации и multi-user isolation. - Discovery просматривает только верхний уровень `datasets/` и `models/`. - Live chart зависит от внешнего Chart.js CDN. - В график попадают максимум три numeric metrics, выбранные callback-ом. - Реальное длительное YOLO-обучение и Docker image build не входят в быстрый test suite; unit-тесты подменяют Ultralytics и subprocess там, где это возможно. - В репозитории нет `.dockerignore` и CI-конфигурации; Docker build context зависит от содержимого рабочей копии. - FastAPI TestClient выдаёт deprecation warning для текущей связки Starlette/httpx; тесты при этом проходят. Отдельного файла лицензии проекта в репозитории нет. README напоминает, что Ultralytics распространяется по AGPL-3.0 и предлагает отдельно проверить условия Enterprise-лицензии для закрытого коммерческого использования. Перед работой с известными дефектами сверяйтесь с `PROJECT_ISSUES.md`: на дату этого контекста перечисленные там 10 проблем исправлены.