diff --git a/.env.example b/.env.example index 51b26eb..63bfc26 100644 --- a/.env.example +++ b/.env.example @@ -36,14 +36,30 @@ CLOUDFLARE_TUNNEL_URL=http://app:3003 # туннеля без VPN (сек). Если VPN-провайдер не отвечает — сайт всё равно поднимется. WG_HANDSHAKE_TIMEOUT=60 -# === ИИ-улучшение фото (контейнер photo-ai, Real-ESRGAN) === +# === ИИ-улучшение фото (контейнер photo-ai, Real-ESRGAN + GFPGAN) === # По умолчанию фото-ИИ включено: сервис photo-ai поднимается вместе со стеком # и работает на CPU. Оставьте значение пустым, чтобы выключить сервис — # тогда кнопка «🤖 ИИ» скрыта, а /api/entries/:id/photo/enhance-ai отвечает 503. # Ручные проверки сервиса идут по loopback-порту 127.0.0.1:8081 (наружу не публикуется): # curl http://127.0.0.1:8081/health PHOTO_AI_URL=http://photo-ai:8080 +# Максимум пикселей входного изображения, более крупный вход уменьшается. PHOTO_AI_MAX_PIXELS=4000000 +# Устройство инференса: auto (CUDA, если контейнеру выдан GPU, иначе CPU), cuda, cpu. +# Явный cuda без доступной CUDA не роняет сервис: WARN в лог и работа на CPU. +# GPU-запуск подключается файлом docker-compose.gpu.yml. +PHOTO_AI_DEVICE=auto +# Размер тайла инференса (0 — без тайлов). Меньше тайл — меньше памяти, медленнее. +PHOTO_AI_TILE=256 +# Модель восстановления лиц: gfpgan (по умолчанию) или codeformer. +# codeformer доступен только если модуль вендорен в photo-ai/vendor. +PHOTO_AI_FACE_MODEL=gfpgan +# Предзагрузка всех моделей при старте (1) или ленивая загрузка по требованию (0). +PHOTO_AI_LOAD_ALL=0 +# Качество JPEG результата (70..100). +PHOTO_AI_JPEG_QUALITY=92 +# Таймаут заданий с восстановлением лиц на стороне приложения (мс). +PHOTO_AI_FACE_TIMEOUT_MS=600000 # Сколько раз воркер повторит задание, если photo-ai недоступен (503/обрыв сети), # не увеличивая attempts; после исчерпания — одна честная ошибка в задании. PHOTO_AI_SOFT_MAX_RETRIES=60 diff --git a/README.md b/README.md index 033e46a..13147c0 100644 --- a/README.md +++ b/README.md @@ -121,14 +121,20 @@ REDIS_PASSWORD=случайная-длинная-строка ## ИИ-улучшение фото (photo-ai) -Сервис `photo-ai` (Real-ESRGAN) поднимается вместе со стеком и **включён по умолчанию**: `PHOTO_AI_URL` -в `docker-compose.yml` равен `http://photo-ai:8080`, кнопка «🤖 ИИ» активна, а задания обрабатывает -фоновый воркер. Работает на CPU, GPU не требуется. +Сервис `photo-ai` (Real-ESRGAN + GFPGAN) поднимается вместе со стеком и **включён по умолчанию**: +`PHOTO_AI_URL` в `docker-compose.yml` равен `http://photo-ai:8080`, кнопка «🤖 ИИ» активна, +а задания обрабатывает фоновый воркер. Работает на CPU, GPU не требуется. | Переменная | По умолчанию | Назначение | |---|---|---| | `PHOTO_AI_URL` | `http://photo-ai:8080` | Адрес сервиса. **Пустое значение = сервис выключен**: кнопка «🤖 ИИ» скрыта, `POST /api/entries/:id/photo/enhance-ai` отвечает `503`, приложение при этом полностью работоспособно | | `PHOTO_AI_MAX_PIXELS` | `4000000` | Максимум пикселей входного изображения, вход большего размера уменьшается | +| `PHOTO_AI_DEVICE` | `auto` | Устройство инференса: `auto` (CUDA, если контейнеру выдан GPU, иначе CPU), `cuda`, `cpu`. Явный `cuda` без CUDA не роняет сервис: WARN в лог и работа на CPU | +| `PHOTO_AI_TILE` | `256` | Размер тайла инференса (`0` — без тайлов): меньше тайл — меньше памяти, но медленнее | +| `PHOTO_AI_FACE_MODEL` | `gfpgan` | Модель восстановления лиц: `gfpgan`; `codeformer` доступен только при вендоринге модуля в `photo-ai/vendor` | +| `PHOTO_AI_LOAD_ALL` | `0` | Загружать все модели при старте (`1`) или лениво по требованию (`0`) | +| `PHOTO_AI_JPEG_QUALITY` | `92` | Качество JPEG результата, 70..100 | +| `PHOTO_AI_FACE_TIMEOUT_MS` | `600000` | Таймаут заданий с восстановлением лиц (мс) | | `PHOTO_AI_SOFT_MAX_RETRIES` | `60` | Сколько раз задание ждёт недоступный сервис, не увеличивая счётчик попыток; после исчерпания — честная ошибка | | `PHOTO_AI_SOFT_BACKOFF_MS` | `10000` | Первая пауза перед мягким повтором | | `PHOTO_AI_SOFT_BACKOFF_MAX_MS` | `300000` | Потолок паузы (задержка растёт вдвое) | diff --git a/TODO_PHOTO_FACE_AI.md b/TODO_PHOTO_FACE_AI.md index ea19b2a..1cbd942 100644 --- a/TODO_PHOTO_FACE_AI.md +++ b/TODO_PHOTO_FACE_AI.md @@ -158,6 +158,10 @@ `opencv-python-headless 5.0.0.93`. До Stage 2 numpy не трогаем (Stage 1 меряет I1 на текущем 2.2.6); в Stage 2 пин либо переносится в один общий вызов `pip install`, либо фиксируется фактическая версия — с обязательной перепроверкой эталона I1 после любого изменения numpy. + - **Закрыто в Stage 2:** требования `numpy<2` и `opencv-python-headless 5.0.0.93` несовместимы + (opencv 5 требует numpy ≥ 2), поэтому зафиксирована фактическая версия `numpy==2.2.6` в том же + вызове `pip install`, что и остальные зависимости, а `opencv-python` (не-headless) больше не + устанавливается вовсе. Эталон I1 перепроверен — 5/5 байт-в-байт (см. «Журнал раздела 2»). - Веса (проверено HEAD): `x2plus` v0.2.1 — 200; `general-x4v3` и `animevideov3` v0.2.5.0 — 200; `codeformer.pth` v0.1.0 — 200. GFPGAN: берём `GFPGANv1.4.pth` из релиза `v1.3.0` (200, `arch='clean'`, `channel_multiplier=2` — как у официального `inference_gfpgan.py -v 1.4`); `GFPGANCleanv1-NoCE-C2.pth` @@ -307,25 +311,77 @@ Stage 0 закрыт 2026-09-28, журнал проверок — «Журна ## 5. Stage 2. `photo-ai/Dockerfile` + compose -- [ ] `ARG TORCH_VARIANT=cpu` и `ARG TORCH_INDEX=https://download.pytorch.org/whl/${TORCH_VARIANT}`; +- [x] `ARG TORCH_VARIANT=cpu` и `ARG TORCH_INDEX=https://download.pytorch.org/whl/${TORCH_VARIANT}`; один образ, `cu124` — вариант сборки. -- [ ] Сохранить патч `basicsr/data/degradations.py` (`functional_tensor` → `functional`) — без него basicsr +- [x] Сохранить патч `basicsr/data/degradations.py` (`functional_tensor` → `functional`) — без него basicsr падает на torch ≥ 2.0. Не «упрощать» Dockerfile без проверки. -- [ ] Сохранить `libgl1 libglib2.0-0` (нужны facexlib), `numpy<2`, `opencv-python-headless`. -- [ ] Добавить `gfpgan`/`facexlib` (или вендоринг по `D4`), вендоренный CodeFormer копировать в образ +- [x] Сохранить `libgl1 libglib2.0-0` (нужны facexlib), `numpy<2`, `opencv-python-headless`. +- [x] Добавить `gfpgan`/`facexlib` (или вендоринг по `D4`), вендоренный CodeFormer копировать в образ при наличии (`D5`). -- [ ] `ENV PHOTO_AI_MODELS_DIR=/models`; `MODEL_PATH` остаётся валидным алиасом. -- [ ] `docker-compose.yml`, сервис `photo-ai`: env `PHOTO_AI_DEVICE`, `PHOTO_AI_FACE_MODEL`, `PHOTO_AI_TILE`, +- [x] `ENV PHOTO_AI_MODELS_DIR=/models`; `MODEL_PATH` остаётся валидным алиасом. +- [x] `docker-compose.yml`, сервис `photo-ai`: env `PHOTO_AI_DEVICE`, `PHOTO_AI_FACE_MODEL`, `PHOTO_AI_TILE`, `PHOTO_AI_MAX_PIXELS`, `PHOTO_AI_LOAD_ALL`, `PHOTO_AI_JPEG_QUALITY`; `healthcheck` с `start_period: 300s`; порт по `D1` уже проброшен на loopback — сохранить. -- [ ] Новый `docker-compose.gpu.yml` (по образцу `docker-compose.minio.yml`): `build.args.TORCH_VARIANT=cu124`, +- [x] Новый `docker-compose.gpu.yml` (по образцу `docker-compose.minio.yml`): `build.args.TORCH_VARIANT=cu124`, `PHOTO_AI_DEVICE=cuda`, `deploy.resources.reservations.devices` (`driver: nvidia`, `count: 1`). Без него стек поднимается на любой машине. -- [ ] Сервису `app` добавить env `PHOTO_AI_FACE_MODEL` и `PHOTO_AI_FACE_TIMEOUT_MS`. -- [ ] **Приёмка:** CPU-сборка стартует; `/api/photo-jobs/status` показывает `service.device`; +- [x] Сервису `app` добавить env `PHOTO_AI_FACE_MODEL` и `PHOTO_AI_FACE_TIMEOUT_MS`. +- [x] **Приёмка:** CPU-сборка стартует; `/api/photo-jobs/status` показывает `service.device`; при пустом `PHOTO_AI_URL` приложение работает полностью (I3); `docker compose -f docker-compose.yml -f docker-compose.gpu.yml config` валиден (сам GPU-пуск — только если toolkit есть). +### Журнал раздела 2 (проверено 2026-09-29) + +Изменены `photo-ai/Dockerfile`, `docker-compose.yml`, добавлены `docker-compose.gpu.yml` и +`photo-ai/vendor/.gitkeep`, а также `.env.example` и таблица переменных в `README.md` — Stage 1 отложил +их именно на Stage 2, потому что раньше compose эти переменные не подставлял. `app.py`, `worker.js`, +`server.js`, схема БД и фронтенд не тронуты — они в Stage 3…6. + +| Проверка | Как | Результат | +|---|---|---| +| CPU-сборка | `docker compose build photo-ai` (слои pip с нуля) | `EXIT=0`, образ `whatido-photo-ai:latest` **2.63 ГБ** (план оценивал ~2.5 ГБ) | +| Состав пакетов | `docker exec photo-ai pip list` | `gfpgan 1.3.8`, `facexlib 0.3.0`, `basicsr 1.4.2`, `realesrgan 0.3.0`, `numpy 2.2.6`, `torch 2.14.0+cpu`, `matplotlib 3.10.9`; `opencv-python-headless 5.0.0.93` — **единственный** cv2; `tb-nightly` и `yapf` отсутствуют | +| Импорты | `python -c "import basicsr, gfpgan, facexlib, realesrgan, cv2"` | ок; cv2 `GUI: NONE` (до этого активной была не-headless сборка с `GUI: QT5`) | +| Патч basicsr | лог сборки | `basicsr patch applied to /usr/local/lib/python3.10/site-packages/basicsr/data/degradations.py` | +| **I1** | `capture.js after-stage2` + `verify.js after-stage2` | **5/5 `MATCH` байт-в-байт** (`795a519b9a9c70f6`, `fe2496f7ef798456`, `fce1949260590855`, `1b52a49d690ca8d7`, `e0e6e480f4799bc0`) — смена cv2 на headless и явный пин numpy результат не изменили | +| `service.device` | `GET /api/photo-jobs/status` (admin) | 200; `service` = `ok/ready/device=cpu/device_name/half/tile/driver/cuda/vram_total_mb/vram_free_mb/models/face_models/loaded/loading/max_pixels` + `configured/reachable/latency_ms/error` | +| healthcheck | `docker inspect photo-ai` | `healthy` после `start_period: 300s`; та же команда вручную в контейнере → exit 0 | +| env контейнера | `docker inspect photo-ai` | `PHOTO_AI_DEVICE=auto`, `PHOTO_AI_TILE=256`, `PHOTO_AI_FACE_MODEL=gfpgan`, `PHOTO_AI_LOAD_ALL=0`, `PHOTO_AI_JPEG_QUALITY=92`, `PHOTO_AI_MAX_PIXELS=4000000`, `PHOTO_AI_MODELS_DIR=/models`, `MODEL_PATH=/models/RealESRGAN_x2plus.pth` | +| env `app` | `docker compose exec app node -e ...` | `PHOTO_AI_URL=http://photo-ai:8080`, `PHOTO_AI_FACE_MODEL=gfpgan`, `PHOTO_AI_FACE_TIMEOUT_MS=600000` | +| I3 | override с `PHOTO_AI_URL: ""` (файл в `/tmp`, вне репозитория) | `photo_ai_enabled=false`, `POST /api/entries/1/photo/enhance-ai` → `503` «ИИ-обработка фото не настроена», 8 маршрутов API → 200; возврат к базовой конфигурации → `photo_ai_enabled=true` | +| I4 (ветка cuda) | `PHOTO_AI_DEVICE=cuda` на новом образе | WARN «PHOTO_AI_DEVICE=cuda, но CUDA недоступна — работаю на CPU», `/health` 200 и `device=cpu` | +| Compose | `docker compose config -q` — база, `+docker-compose.minio.yml`, `+docker-compose.gpu.yml` | три конфигурации валидны; GPU-оверрайд даёт `TORCH_VARIANT=cu124`, `PHOTO_AI_DEVICE=cuda` и `deploy.resources.reservations.devices[driver=nvidia, count=1, capabilities=[gpu]]` | +| GPU-пуск | не выполнялся | `nvidia-ctk` на хосте отсутствует — стоп-условие: установка toolkit только с разрешения оператора | + +Решения и находки, которые нужно знать дальше: + +- **`numpy<2` был невыполним.** `opencv-python-headless 5.0.0.93` требует numpy ≥ 2, поэтому пин заменён + на фактическую версию `numpy==2.2.6` и перенесён в тот же вызов `pip install`, что и остальные + зависимости (раньше пин стоял отдельным вызовом, и следующие установки его игнорировали). I1 после + изменения numpy перепроверен — байт-в-байт. +- **`--no-deps` для `basicsr`/`realesrgan`/`gfpgan`/`facexlib`** убирает dev-зависимости gfpgan + (`tb-nightly`, `yapf`). Проверено, что в рантайме они не нужны: `tensorboard` в basicsr импортируется + только лениво внутри `init_tb_logger`/`read_data_from_tensorboard`, `yapf` не импортируется вовсе, + а `matplotlib` приходит как зависимость `filterpy` (требование facexlib) и в образе остался. +- **`opencv-python` (не-headless) больше не ставится.** Раньше он приходил транзитивно через gfpgan и + перезаписывал headless-сборку (активным был cv2 с `GUI: QT5`). Теперь cv2 один, headless, и JPEG-байты + совпали с эталоном — GUI-обвязка на результат не влияла. +- **`libgl1 libglib2.0-0` оставлены** по требованию плана (facexlib), хотя при headless cv2 они нужны + меньше: экономия здесь не стоит риска незамеченной регрессии. +- **`photo-ai/vendor/.gitkeep`** — каталог вендоринга существует в репозитории, поэтому + `COPY vendor/ ./vendor/` не падает на сборке без CodeFormer, а положенный туда модуль подхватывается + `sys.path` в `app.py` без правок Dockerfile (`D5`). +- **env `PHOTO_AI_FACE_MODEL`/`PHOTO_AI_FACE_TIMEOUT_MS` у `app`** добавлены по чек-листу Stage 2; + `PHOTO_AI_FACE_MODEL` уже используется `app.py` как дефолт face-модели, воркер начнёт читать оба + в Stage 4 (сейчас `ai_timeout_ms` в `worker.config` всё ещё 300000 — это ожидаемо). +- **`TORCH_VARIANT` в `docker-compose.gpu.yml` захардкожен (`cu124`)**, а не берётся из `.env`: иначе + `TORCH_VARIANT=cpu` в `.env` молча собирал бы GPU-конфигурацию без CUDA. +- **`MAX_INPUT_PIXELS` в compose заменён на канонический `PHOTO_AI_MAX_PIXELS`**; `app.py` по-прежнему + читает старое имя как алиас, так что существующий `.env` не ломается. Порт `8081` на loopback (`D1`) + сохранён. +- **`.env.example`/`README.md`** описывают ровно те переменные, которые подставляет compose; блок про + GPU-запуск и установку NVIDIA Container Toolkit остаётся за Stage 6 (решение `D6`). + --- ## 6. Stage 3. Face-режим (GFPGAN) в `app.py` diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml new file mode 100644 index 0000000..34e9883 --- /dev/null +++ b/docker-compose.gpu.yml @@ -0,0 +1,23 @@ +# Переопределение photo-ai для работы на NVIDIA GPU. +# Использование: +# docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build photo-ai +# +# Требуется драйвер NVIDIA и установленный NVIDIA Container Toolkit +# (nvidia-ctk + nvidia-container-runtime). Без toolkit контейнер не стартует — +# базовый docker-compose.yml остаётся CPU-сборкой и этот файл не подключается. +# PHOTO_AI_DEVICE=cuda при недоступной CUDA не роняет сервис: app.py пишет WARN +# и работает на CPU, /health при этом отвечает 200. +services: + photo-ai: + build: + args: + TORCH_VARIANT: cu124 + environment: + PHOTO_AI_DEVICE: ${PHOTO_AI_DEVICE:-cuda} + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] diff --git a/docker-compose.yml b/docker-compose.yml index 706866d..6d2cefd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -77,6 +77,8 @@ services: AI_PROMPT: ${AI_PROMPT:-} AI_REQUEST_TIMEOUT_MS: ${AI_REQUEST_TIMEOUT_MS:-120000} PHOTO_AI_URL: ${PHOTO_AI_URL:-http://photo-ai:8080} + PHOTO_AI_FACE_MODEL: ${PHOTO_AI_FACE_MODEL:-gfpgan} + PHOTO_AI_FACE_TIMEOUT_MS: ${PHOTO_AI_FACE_TIMEOUT_MS:-600000} NODE_ENV: production TZ: Europe/Moscow STORAGE_DRIVER: ${STORAGE_DRIVER:-local} @@ -189,7 +191,9 @@ services: - "8080:8080" - # ИИ-улучшение фотографий (Real-ESRGAN x2, CPU). + # ИИ-улучшение фотографий (Real-ESRGAN: апскейл, денойз, восстановление лиц GFPGAN). + # Устройство выбирается автоматически (PHOTO_AI_DEVICE=auto): CUDA, если контейнеру + # выдан GPU, иначе CPU. Запуск на GPU — через docker-compose.gpu.yml. # Поднимается вместе со стеком; если не нужен — PHOTO_AI_URL пустой в .env. # Порт 8081 пробрасывается только на loopback хоста — наружу ничего не публикуется, # хостовый 8080 уже занят text-corrector. Ручные проверки: curl http://127.0.0.1:8081/health @@ -199,12 +203,24 @@ services: restart: unless-stopped environment: MODEL_PATH: /models/RealESRGAN_x2plus.pth - MAX_INPUT_PIXELS: ${PHOTO_AI_MAX_PIXELS:-4000000} + PHOTO_AI_MODELS_DIR: /models + PHOTO_AI_MAX_PIXELS: ${PHOTO_AI_MAX_PIXELS:-4000000} + PHOTO_AI_DEVICE: ${PHOTO_AI_DEVICE:-auto} + PHOTO_AI_TILE: ${PHOTO_AI_TILE:-256} + PHOTO_AI_FACE_MODEL: ${PHOTO_AI_FACE_MODEL:-gfpgan} + PHOTO_AI_LOAD_ALL: ${PHOTO_AI_LOAD_ALL:-0} + PHOTO_AI_JPEG_QUALITY: ${PHOTO_AI_JPEG_QUALITY:-92} TZ: Europe/Moscow volumes: - photo-ai-models:/models ports: - "127.0.0.1:8081:8080" + healthcheck: + test: ["CMD", "python", "-c", "import sys, urllib.request; r = urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=5); sys.exit(0 if r.status == 200 else 1)"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 300s volumes: diff --git a/photo-ai/Dockerfile b/photo-ai/Dockerfile index 157efc5..7d1254c 100644 --- a/photo-ai/Dockerfile +++ b/photo-ai/Dockerfile @@ -1,14 +1,19 @@ FROM python:3.10-slim +ARG TORCH_VARIANT=cpu +ARG TORCH_INDEX=https://download.pytorch.org/whl/${TORCH_VARIANT} + WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* -RUN pip install --no-cache-dir "typing-extensions==4.12.2" "numpy<2" +RUN pip install --no-cache-dir "typing-extensions==4.12.2" "numpy==2.2.6" -RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu +RUN pip install --no-cache-dir torch torchvision --index-url ${TORCH_INDEX} -RUN pip install --no-cache-dir realesrgan==0.3.0 fastapi "uvicorn[standard]" python-multipart opencv-python-headless +RUN pip install --no-cache-dir --no-deps basicsr==1.4.2 realesrgan==0.3.0 gfpgan==1.3.8 facexlib==0.3.0 && \ + pip install --no-cache-dir "numpy==2.2.6" "opencv-python-headless==5.0.0.93" addict future lmdb Pillow pyyaml \ + requests scikit-image scipy tqdm filterpy numba fastapi "uvicorn[standard]" python-multipart RUN BASICSR_DEG=$(python -c "import basicsr; import os; print(os.path.join(os.path.dirname(basicsr.__file__), 'data', 'degradations.py'))" 2>/dev/null) || \ BASICSR_DEG=$(find /usr/local/lib/python3.10 -path "*/basicsr/data/degradations.py" 2>/dev/null | head -1) && \ @@ -19,8 +24,10 @@ RUN BASICSR_DEG=$(python -c "import basicsr; import os; print(os.path.join(os.pa echo "basicsr degradations.py not found, skipping patch"; \ fi -COPY app.py . +COPY app.py ./ +COPY vendor/ ./vendor/ +ENV PHOTO_AI_MODELS_DIR=/models ENV MODEL_PATH=/models/RealESRGAN_x2plus.pth VOLUME /models diff --git a/photo-ai/vendor/.gitkeep b/photo-ai/vendor/.gitkeep new file mode 100644 index 0000000..e69de29