build(photo-ai): CPU/GPU сборка, gfpgan+facexlib без dev-зависимостей, healthcheck
Stage 2: photo-ai/Dockerfile получил ARG TORCH_VARIANT/TORCH_INDEX (один образ, cu124 — вариант сборки) и явные пины gfpgan==1.3.8 / facexlib==0.3.0 через --no-deps: tb-nightly и yapf (dev-зависимости gfpgan/basicsr) больше не попадают в рантайм, matplotlib остаётся как зависимость filterpy (требование facexlib). Патч basicsr/data/degradations.py сохранён байт-в-байт — без него basicsr падает на torch >= 2.0. Пин numpy<2 был невыполним: opencv-python-headless 5.0.0.93 требует numpy >= 2. Зафиксирована фактическая версия numpy==2.2.6 в общем вызове pip install, а opencv-python (не-headless) больше не ставится — раньше он приходил через gfpgan и перезаписывал headless-сборку (активным был cv2 с GUI: QT5). I1 после этого перепроверен: 5/5 MATCH байт-в-байт против эталона Stage 0. ENV PHOTO_AI_MODELS_DIR=/models, MODEL_PATH остаётся валидным алиасом. COPY vendor/ ./vendor/ + vendor/.gitkeep — вендоренный CodeFormer (D5) подхватится из sys.path без правок Dockerfile. docker-compose.yml: у photo-ai новые env (DEVICE, TILE, FACE_MODEL, LOAD_ALL, JPEG_QUALITY, MODELS_DIR) и healthcheck со start_period 300s; MAX_INPUT_PIXELS заменён на канонический PHOTO_AI_MAX_PIXELS (старое имя читается app.py как алиас). У app — PHOTO_AI_FACE_MODEL и PHOTO_AI_FACE_TIMEOUT_MS (воркер читает их в Stage 4). Новый docker-compose.gpu.yml (по образцу minio): TORCH_VARIANT=cu124, PHOTO_AI_DEVICE=cuda, deploy.resources.reservations.devices для nvidia. .env.example и таблица переменных README описывают ровно те переменные, которые теперь подставляет compose; блок про GPU и NVIDIA Container Toolkit — Stage 6 (D6). Проверено на живом стеке: сборка EXIT=0 (2.63 ГБ), /health отдаёт device=cpu, face_models=[gfpgan], контейнер healthy, /api/photo-jobs/status отдаёт service.device, I3 (пустой PHOTO_AI_URL -> 503 + 8 маршрутов 200), I4 (PHOTO_AI_DEVICE=cuda без CUDA -> WARN + CPU), docker compose config валиден для базы, minio и gpu. GPU-пуск не выполнялся: nvidia-ctk на хосте отсутствует.
This commit is contained in:
+17
-1
@@ -36,14 +36,30 @@ CLOUDFLARE_TUNNEL_URL=http://app:3003
|
|||||||
# туннеля без VPN (сек). Если VPN-провайдер не отвечает — сайт всё равно поднимется.
|
# туннеля без VPN (сек). Если VPN-провайдер не отвечает — сайт всё равно поднимется.
|
||||||
WG_HANDSHAKE_TIMEOUT=60
|
WG_HANDSHAKE_TIMEOUT=60
|
||||||
|
|
||||||
# === ИИ-улучшение фото (контейнер photo-ai, Real-ESRGAN) ===
|
# === ИИ-улучшение фото (контейнер photo-ai, Real-ESRGAN + GFPGAN) ===
|
||||||
# По умолчанию фото-ИИ включено: сервис photo-ai поднимается вместе со стеком
|
# По умолчанию фото-ИИ включено: сервис photo-ai поднимается вместе со стеком
|
||||||
# и работает на CPU. Оставьте значение пустым, чтобы выключить сервис —
|
# и работает на CPU. Оставьте значение пустым, чтобы выключить сервис —
|
||||||
# тогда кнопка «🤖 ИИ» скрыта, а /api/entries/:id/photo/enhance-ai отвечает 503.
|
# тогда кнопка «🤖 ИИ» скрыта, а /api/entries/:id/photo/enhance-ai отвечает 503.
|
||||||
# Ручные проверки сервиса идут по loopback-порту 127.0.0.1:8081 (наружу не публикуется):
|
# Ручные проверки сервиса идут по loopback-порту 127.0.0.1:8081 (наружу не публикуется):
|
||||||
# curl http://127.0.0.1:8081/health
|
# curl http://127.0.0.1:8081/health
|
||||||
PHOTO_AI_URL=http://photo-ai:8080
|
PHOTO_AI_URL=http://photo-ai:8080
|
||||||
|
# Максимум пикселей входного изображения, более крупный вход уменьшается.
|
||||||
PHOTO_AI_MAX_PIXELS=4000000
|
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/обрыв сети),
|
# Сколько раз воркер повторит задание, если photo-ai недоступен (503/обрыв сети),
|
||||||
# не увеличивая attempts; после исчерпания — одна честная ошибка в задании.
|
# не увеличивая attempts; после исчерпания — одна честная ошибка в задании.
|
||||||
PHOTO_AI_SOFT_MAX_RETRIES=60
|
PHOTO_AI_SOFT_MAX_RETRIES=60
|
||||||
|
|||||||
@@ -121,14 +121,20 @@ REDIS_PASSWORD=случайная-длинная-строка
|
|||||||
|
|
||||||
## ИИ-улучшение фото (photo-ai)
|
## ИИ-улучшение фото (photo-ai)
|
||||||
|
|
||||||
Сервис `photo-ai` (Real-ESRGAN) поднимается вместе со стеком и **включён по умолчанию**: `PHOTO_AI_URL`
|
Сервис `photo-ai` (Real-ESRGAN + GFPGAN) поднимается вместе со стеком и **включён по умолчанию**:
|
||||||
в `docker-compose.yml` равен `http://photo-ai:8080`, кнопка «🤖 ИИ» активна, а задания обрабатывает
|
`PHOTO_AI_URL` в `docker-compose.yml` равен `http://photo-ai:8080`, кнопка «🤖 ИИ» активна,
|
||||||
фоновый воркер. Работает на CPU, GPU не требуется.
|
а задания обрабатывает фоновый воркер. Работает на CPU, GPU не требуется.
|
||||||
|
|
||||||
| Переменная | По умолчанию | Назначение |
|
| Переменная | По умолчанию | Назначение |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `PHOTO_AI_URL` | `http://photo-ai:8080` | Адрес сервиса. **Пустое значение = сервис выключен**: кнопка «🤖 ИИ» скрыта, `POST /api/entries/:id/photo/enhance-ai` отвечает `503`, приложение при этом полностью работоспособно |
|
| `PHOTO_AI_URL` | `http://photo-ai:8080` | Адрес сервиса. **Пустое значение = сервис выключен**: кнопка «🤖 ИИ» скрыта, `POST /api/entries/:id/photo/enhance-ai` отвечает `503`, приложение при этом полностью работоспособно |
|
||||||
| `PHOTO_AI_MAX_PIXELS` | `4000000` | Максимум пикселей входного изображения, вход большего размера уменьшается |
|
| `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_MAX_RETRIES` | `60` | Сколько раз задание ждёт недоступный сервис, не увеличивая счётчик попыток; после исчерпания — честная ошибка |
|
||||||
| `PHOTO_AI_SOFT_BACKOFF_MS` | `10000` | Первая пауза перед мягким повтором |
|
| `PHOTO_AI_SOFT_BACKOFF_MS` | `10000` | Первая пауза перед мягким повтором |
|
||||||
| `PHOTO_AI_SOFT_BACKOFF_MAX_MS` | `300000` | Потолок паузы (задержка растёт вдвое) |
|
| `PHOTO_AI_SOFT_BACKOFF_MAX_MS` | `300000` | Потолок паузы (задержка растёт вдвое) |
|
||||||
|
|||||||
+65
-9
@@ -158,6 +158,10 @@
|
|||||||
`opencv-python-headless 5.0.0.93`. До Stage 2 numpy не трогаем (Stage 1 меряет I1 на текущем
|
`opencv-python-headless 5.0.0.93`. До Stage 2 numpy не трогаем (Stage 1 меряет I1 на текущем
|
||||||
2.2.6); в Stage 2 пин либо переносится в один общий вызов `pip install`, либо фиксируется
|
2.2.6); в Stage 2 пин либо переносится в один общий вызов `pip install`, либо фиксируется
|
||||||
фактическая версия — с обязательной перепроверкой эталона I1 после любого изменения numpy.
|
фактическая версия — с обязательной перепроверкой эталона 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;
|
- Веса (проверено 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'`,
|
`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`
|
`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
|
## 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` — вариант сборки.
|
один образ, `cu124` — вариант сборки.
|
||||||
- [ ] Сохранить патч `basicsr/data/degradations.py` (`functional_tensor` → `functional`) — без него basicsr
|
- [x] Сохранить патч `basicsr/data/degradations.py` (`functional_tensor` → `functional`) — без него basicsr
|
||||||
падает на torch ≥ 2.0. Не «упрощать» Dockerfile без проверки.
|
падает на torch ≥ 2.0. Не «упрощать» Dockerfile без проверки.
|
||||||
- [ ] Сохранить `libgl1 libglib2.0-0` (нужны facexlib), `numpy<2`, `opencv-python-headless`.
|
- [x] Сохранить `libgl1 libglib2.0-0` (нужны facexlib), `numpy<2`, `opencv-python-headless`.
|
||||||
- [ ] Добавить `gfpgan`/`facexlib` (или вендоринг по `D4`), вендоренный CodeFormer копировать в образ
|
- [x] Добавить `gfpgan`/`facexlib` (или вендоринг по `D4`), вендоренный CodeFormer копировать в образ
|
||||||
при наличии (`D5`).
|
при наличии (`D5`).
|
||||||
- [ ] `ENV PHOTO_AI_MODELS_DIR=/models`; `MODEL_PATH` остаётся валидным алиасом.
|
- [x] `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] `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` с
|
`PHOTO_AI_MAX_PIXELS`, `PHOTO_AI_LOAD_ALL`, `PHOTO_AI_JPEG_QUALITY`; `healthcheck` с
|
||||||
`start_period: 300s`; порт по `D1` уже проброшен на loopback — сохранить.
|
`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`).
|
`PHOTO_AI_DEVICE=cuda`, `deploy.resources.reservations.devices` (`driver: nvidia`, `count: 1`).
|
||||||
Без него стек поднимается на любой машине.
|
Без него стек поднимается на любой машине.
|
||||||
- [ ] Сервису `app` добавить env `PHOTO_AI_FACE_MODEL` и `PHOTO_AI_FACE_TIMEOUT_MS`.
|
- [x] Сервису `app` добавить env `PHOTO_AI_FACE_MODEL` и `PHOTO_AI_FACE_TIMEOUT_MS`.
|
||||||
- [ ] **Приёмка:** CPU-сборка стартует; `/api/photo-jobs/status` показывает `service.device`;
|
- [x] **Приёмка:** CPU-сборка стартует; `/api/photo-jobs/status` показывает `service.device`;
|
||||||
при пустом `PHOTO_AI_URL` приложение работает полностью (I3); `docker compose -f docker-compose.yml
|
при пустом `PHOTO_AI_URL` приложение работает полностью (I3); `docker compose -f docker-compose.yml
|
||||||
-f docker-compose.gpu.yml config` валиден (сам GPU-пуск — только если toolkit есть).
|
-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`
|
## 6. Stage 3. Face-режим (GFPGAN) в `app.py`
|
||||||
|
|||||||
@@ -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]
|
||||||
+18
-2
@@ -77,6 +77,8 @@ services:
|
|||||||
AI_PROMPT: ${AI_PROMPT:-}
|
AI_PROMPT: ${AI_PROMPT:-}
|
||||||
AI_REQUEST_TIMEOUT_MS: ${AI_REQUEST_TIMEOUT_MS:-120000}
|
AI_REQUEST_TIMEOUT_MS: ${AI_REQUEST_TIMEOUT_MS:-120000}
|
||||||
PHOTO_AI_URL: ${PHOTO_AI_URL:-http://photo-ai:8080}
|
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
|
NODE_ENV: production
|
||||||
TZ: Europe/Moscow
|
TZ: Europe/Moscow
|
||||||
STORAGE_DRIVER: ${STORAGE_DRIVER:-local}
|
STORAGE_DRIVER: ${STORAGE_DRIVER:-local}
|
||||||
@@ -189,7 +191,9 @@ services:
|
|||||||
- "8080:8080"
|
- "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.
|
# Поднимается вместе со стеком; если не нужен — PHOTO_AI_URL пустой в .env.
|
||||||
# Порт 8081 пробрасывается только на loopback хоста — наружу ничего не публикуется,
|
# Порт 8081 пробрасывается только на loopback хоста — наружу ничего не публикуется,
|
||||||
# хостовый 8080 уже занят text-corrector. Ручные проверки: curl http://127.0.0.1:8081/health
|
# хостовый 8080 уже занят text-corrector. Ручные проверки: curl http://127.0.0.1:8081/health
|
||||||
@@ -199,12 +203,24 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
MODEL_PATH: /models/RealESRGAN_x2plus.pth
|
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
|
TZ: Europe/Moscow
|
||||||
volumes:
|
volumes:
|
||||||
- photo-ai-models:/models
|
- photo-ai-models:/models
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8081:8080"
|
- "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:
|
volumes:
|
||||||
|
|||||||
+11
-4
@@ -1,14 +1,19 @@
|
|||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
ARG TORCH_VARIANT=cpu
|
||||||
|
ARG TORCH_INDEX=https://download.pytorch.org/whl/${TORCH_VARIANT}
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
|
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) || \
|
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) && \
|
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"; \
|
echo "basicsr degradations.py not found, skipping patch"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
COPY app.py .
|
COPY app.py ./
|
||||||
|
COPY vendor/ ./vendor/
|
||||||
|
|
||||||
|
ENV PHOTO_AI_MODELS_DIR=/models
|
||||||
ENV MODEL_PATH=/models/RealESRGAN_x2plus.pth
|
ENV MODEL_PATH=/models/RealESRGAN_x2plus.pth
|
||||||
|
|
||||||
VOLUME /models
|
VOLUME /models
|
||||||
|
|||||||
Vendored
Reference in New Issue
Block a user