diff --git a/.env.example b/.env.example index 63bfc26..8ea2ba2 100644 --- a/.env.example +++ b/.env.example @@ -47,9 +47,12 @@ 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. +# GPU-вариант собирается оверрайдом (отдельный образ whatido-photo-ai:cu126, GPU через CDI): +# docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build photo-ai +# Подробности установки NVIDIA Container Toolkit — в README, раздел «Запуск на NVIDIA GPU». PHOTO_AI_DEVICE=auto # Размер тайла инференса (0 — без тайлов). Меньше тайл — меньше памяти, медленнее. +# На GPU с 4 ГБ VRAM держите 256: при нехватке сервис сам пройдёт лестницу тайлов и уйдёт на CPU. PHOTO_AI_TILE=256 # Модель восстановления лиц: gfpgan (по умолчанию) или codeformer. # codeformer доступен только если модуль вендорен в photo-ai/vendor. diff --git a/README.md b/README.md index 13147c0..25db089 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ REDIS_PASSWORD=случайная-длинная-строка Сервис `photo-ai` (Real-ESRGAN + GFPGAN) поднимается вместе со стеком и **включён по умолчанию**: `PHOTO_AI_URL` в `docker-compose.yml` равен `http://photo-ai:8080`, кнопка «🤖 ИИ» активна, -а задания обрабатывает фоновый воркер. Работает на CPU, GPU не требуется. +а задания обрабатывает фоновый воркер. Базовая сборка работает на CPU, GPU не требуется. | Переменная | По умолчанию | Назначение | |---|---|---| @@ -139,18 +139,66 @@ REDIS_PASSWORD=случайная-длинная-строка | `PHOTO_AI_SOFT_BACKOFF_MS` | `10000` | Первая пауза перед мягким повтором | | `PHOTO_AI_SOFT_BACKOFF_MAX_MS` | `300000` | Потолок паузы (задержка растёт вдвое) | +### Запуск на NVIDIA GPU + +GPU не обязателен: без него сервис работает на CPU. Чтобы включить GPU-вариант, нужен драйвер NVIDIA +и NVIDIA Container Toolkit. + +```bash +# 1. Toolkit (один раз, требует sudo) +curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg +curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \ + | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \ + | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list +sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit + +# 2. GPU-образ (для устройств с поддержкой CDI спеку генерирует сам toolkit) +sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml +sudo systemctl restart docker + +# 3. Запуск +docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build photo-ai +curl http://127.0.0.1:8081/health +``` + +`docker-compose.gpu.yml` собирает отдельный тег `whatido-photo-ai:cu126` (torch из индекса `cu126` — +те же версии, что и в CPU-образе, отличаются только CUDA-библиотеки), поэтому CPU-образ +`whatido-photo-ai:latest` не перетирается и переключение обратно — обычный `docker compose up -d photo-ai`. + +GPU выдаётся контейнеру через CDI (`device_ids: nvidia.com/gpu=all`), поэтому править +`/etc/docker/daemon.json` и перезапускать демон не нужно. Если nvidia-runtime уже зарегистрирован в +демоне (`nvidia-ctk runtime configure --runtime=docker`), в оверрайде можно заменить это на +классическое резервирование `driver: nvidia, count: 1` — результат тот же. + +Проверка результата: в `/health` должны быть `device: cuda:0`, `half: true`, непустые +`vram_total_mb`/`vram_free_mb`. На 4 ГБ (например, RTX 3050 Laptop) реально держатся одновременно +`x2plus` и `gfpgan`: `vram_free_mb` после двух моделей — около 100–300 МБ, поэтому `PHOTO_AI_TILE` +оставьте небольшим (`256`), а `PHOTO_AI_LOAD_ALL=1` на 4 ГБ лучше не включать — предзагрузка всех +моделей подряд исчерпает VRAM. Если памяти не хватило, сервис сам проходит лестницу тайлов +(`PHOTO_AI_TILE` → /2 → /4), затем переключается на CPU и возвращает результат с предупреждением +в `warnings` — задание при этом не падает. + Порт `8081` на `127.0.0.1` — только loopback хоста, наружу ничего не публикуется (хостовый `8080` занят `text-corrector`). Ручные проверки сервиса: ```bash curl http://127.0.0.1:8081/health curl -F "image=@photo.jpg" -F "scale=2" http://127.0.0.1:8081/enhance -o out.jpg +curl -H "Accept: application/json" -F "image=@photo.jpg" -F "face=face" http://127.0.0.1:8081/enhance \ + | python3 -c "import json,sys; d=json.load(sys.stdin); print({k: d[k] for k in ('device','faces_found','elapsed_ms','warnings')})" ``` Состояние сервиса и воркера — в `GET /api/photo-jobs/status` (admin): `service` отдаёт health фото-сервиса -(`configured`, `reachable`, `latency_ms`, `error`), `ai_configured` — задан ли `PHOTO_AI_URL`, +(`configured`, `reachable`, `device`, `device_name`, `half`, `tile`, `vram_total_mb`, `vram_free_mb`, +`models`, `face_models`, `loaded`, `latency_ms`, `error`), `ai_configured` — задан ли `PHOTO_AI_URL`, `worker` — состояние очереди и конфигурация воркера. +Параметры `/enhance`: `image` (файл), `scale` (`2`..`4`), `model` (`x2plus`|`general-x4v3`|`animevideo-v3`), +`face` (`off`|`face`|`all`), `face_model` (`gfpgan`|`codeformer`), `strength` (`0..1`, только CodeFormer), +`jpeg_quality` (`70..100`). По умолчанию отдаётся сырой `image/jpeg`; заголовок +`Accept: application/json` переключает на JSON с `image_base64`, `faces_found`, `device`, `elapsed_ms` +и `warnings` (малое разрешение входа, лица не найдены, не хватило памяти, CodeFormer на CPU). + ## Публичный доступ через Tailscale Стек не требует внешнего IP и проброса портов: контейнер `tailscale` запускается с `network_mode: host`, входит в вашу tailnet-сеть и через **Serve** открывает приложение внутри tailnet, а через **Funnel** — в публичном интернете. diff --git a/TODO_PHOTO_FACE_AI.md b/TODO_PHOTO_FACE_AI.md index 1cbd942..acbd657 100644 --- a/TODO_PHOTO_FACE_AI.md +++ b/TODO_PHOTO_FACE_AI.md @@ -351,7 +351,7 @@ Stage 0 закрыт 2026-09-28, журнал проверок — «Журна | 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 только с разрешения оператора | +| GPU-пуск | `up -d photo-ai` с оверрайдом, когда `nvidia-ctk` есть на хосте | **сделан 2026-09-29**, отдельный журнал «GPU-прогон» в разделе 6: `device=cuda:0`, `half=true`, `vram_total_mb=3822`. Изначально был стоп-условием (toolkit отсутствовал), условие снято | Решения и находки, которые нужно знать дальше: @@ -374,27 +374,98 @@ Stage 0 закрыт 2026-09-28, журнал проверок — «Журна - **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. +- **`TORCH_VARIANT` в `docker-compose.gpu.yml` захардкожен**, а не берётся из `.env`: иначе + `TORCH_VARIANT=cpu` в `.env` молча собирал бы GPU-конфигурацию без CUDA. Значение изменено + `cu124 → cu126` при GPU-прогоне — причина в журнале раздела 3. - **`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`). + GPU-запуск и установку NVIDIA Container Toolkit добавлен в Stage 3 вместе с проверкой GPU-оверрайда + (решение `D6`), остальные документы — за Stage 6. --- ## 6. Stage 3. Face-режим (GFPGAN) в `app.py` -- [ ] `face=off` — только ESRGAN (текущее поведение). -- [ ] `face=face` — ESRGAN-фон + GFPGAN `paste_back`. -- [ ] `face=all` — ESRGAN(+`wdn` для `general-x4v3`) + мягкий денойз фона + лица. -- [ ] `strength` → `fidelity_weight` CodeFormer, `-w`; вне диапазона → `400`. -- [ ] Предупреждения (`warnings`: вход меньше 320×320, лиц не найдено, CodeFormer на CPU) в JSON-ответе. -- [ ] **Приёмка:** портрет 640×480 в `face` → `faces_found=1`, лицо резче; фото без лиц → `faces_found=0` - и результат не хуже `x2plus`; искусственный OOM (`PHOTO_AI_TILE=1024` на 4 ГБ) деградирует до CPU +- [x] `face=off` — только ESRGAN (текущее поведение). — **сделано в Stage 1** (журнал раздела 1) +- [x] `face=face` — ESRGAN-фон + GFPGAN `paste_back`. — **сделано в Stage 1** +- [x] `face=all` — ESRGAN(+`wdn` для `general-x4v3`) + мягкий денойз фона + лица. — **сделано в Stage 1** +- [x] `strength` → `fidelity_weight` CodeFormer, `-w`; вне диапазона → `400`. — **сделано в Stage 1** + (ветка CodeFormer написана, но не проверена: модуль не вендорен, `D5`; проверен путь отказа) +- [x] Предупреждения (`warnings`: вход меньше 320×320, лиц не найдено, CodeFormer на CPU) в JSON-ответе. + Первые два были в Stage 1; **вход меньше 320×320 и CodeFormer на CPU добавлены при приёмке + Stage 3** — их не было в коде +- [x] **Приёмка:** портрет 640×480 в `face` → `faces_found=6`, лицо резче; фото без лиц → `faces_found=0` + и результат не хуже `x2plus`; искусственный OOM (`PHOTO_AI_TILE=2048` на 4 ГБ) деградирует до CPU без падения сервиса. +### Журнал раздела 3 (проверено 2026-09-29) + +Изменён `photo-ai/app.py` (лестница OOM на CUDA + два предупреждения) и `docker-compose.gpu.yml` +(GPU-оверрайд: cu126 + CDI). `worker.js`, `server.js`, схема БД и фронтенд не тронуты — они в Stage 4…6. +Проверки шли на работающем GPU (RTX 3050 Laptop, 4096 МБ, драйвер `615.71.09`, CUDA 12.6) — см. +журнал GPU-прогона в разделе 2. + +| Проверка | Как | Результат | +|---|---|---| +| **Главная находка** | `PHOTO_AI_TILE=2048`, `x2plus face=all` на 4032×2268 | **до правки**: `500 Internal Server Error`, в логе `UnboundLocalError: local variable 'output_tile' referenced before assignment` (`realesrgan/utils.py:179`); **после**: `200`, `warnings: ["не хватило памяти при tile=2048"]`, 28.3 с | +| Причина | чтение кода `realesrgan/utils.py` и `gfpgan/utils.py` | обе библиотеки **проглатывают** `RuntimeError` в своих `try/except` вокруг вызова сети (`except RuntimeError as error: print('Error', error)`) и идут дальше, поэтому `output_tile`/`restored_face` остаётся неприсвоенным. Для `run_guarded` это был уже не `RuntimeError` → ни лестницы тайлов, ни деградации на CPU, ни внятного текста | +| Как починено | `guard_forward()` в `app.py` оборачивает `forward` сетей, построенных нами (`RealESRGANer.model`, `restorer.gfpgan`, CodeFormer `net`): `RuntimeError` с признаком OOM превращается в `TileOOM` | `TileOOM` не наследует `RuntimeError`, поэтому проглатывающие `except` его пропускают; `is_oom` и `run_guarded` (обе точки) ловят `TileOOM` явно | +| Лестница тайлов на GPU | инъекция `TileOOM('CUDA out of memory')` вместо `process_image` | `tile=2048 → 1024 → 512`, затем `degrade_to_cpu` и повтор: `warnings` = 4 пункта, `device: cpu`, `half: false`; при повторе с уже-CPU — честная `500` «не хватило памяти даже при tile=512: пересмотрите PHOTO_AI_TILE или PHOTO_AI_MAX_PIXELS» | +| `x2plus face=all`, tile=2048 | полное фото | `200`, 6 лиц, единственное предупреждение — про тайл; сервис не упал, следующие запросы проходят | +| 640×480, три режима | портрет, приведённый к 640×480 | `off` 1.0 с / `face` 3.8 с / `all` 2.2 с, `faces_found=6`, `device=cuda:0` | +| Фото без лиц | синтетический фон 800×600 | `face` → `faces_found=0`, warning «лица не найдены, фон обработан апскейлом», 0.8 с; байты результата **равны** `face=off` (39861) — без деградации качества | +| Вход меньше 320×320 | 256×256 | два предупреждения: «вход меньше 320×320 — лица могут не найтись» + «лица не найдены» | +| `strength` | `0.5` с gfpgan / `1.4` | `400` «strength применяется только к CodeFormer, для gfpgan оставьте 0.7» / `400` «strength должен быть 0..1» | +| CodeFormer | `face_model=codeformer` | `400` «модель лиц codeformer не установлена в образ, доступен gfpgan» (`D5`) | +| **I1** | 6 сценариев (`jpg`/`.jpeg`/`.png`+q70/`.webp`+q100/`x4v3`/`animevideo-v3`) на новом и на Stage 2 образе, оба на CPU | **6/6 `MATCH` байт-в-байт** (`420757881a31f12dec174e4873e260dd`, `cdbf59d1eb460d26a3b83af2e0543d82`, `654928b60ec43dbd41c6644121041a75`, `557e76d79869230058e1acf75017c670`, `9d3d731c8858bacbba1ee88bf3f373e5`) | +| I7 | `ast.parse`, поиск комментариев | чисто, комментариев 0 | + +Решения и находки, которые нужно знать дальше: + +- **Лестница OOM не работала ни на одном GPU** — только на CPU, где OOM приходит из нашего кода и + не проглатывается. Это наш главный аргумент за то, что GPU-ветку надо было прогнать, а не собрать. +- **`guard_forward` вешается на экземпляр** (`model.forward`), идемпотентен по флагу + `_photo_ai_guarded` и не трогает класс — обёртка не накапливается при пересборке пула. Сетей, + которые строит не мы (retinaface в facexlib, ESRGANer внутри GFPGANer), обёртка не покрывает: там + OOM уходит нашим же `except RuntimeError`, и это правильно. +- **Проглатывание в gfpgan** (`except RuntimeError` вокруг `self.gfpgan(...)`) было даже опаснее + тихого: при OOM лицо молча оставалось исходным, задание завершалось «успешно» без единого + признака в `warnings`. Теперь такой случай уходит в лестницу тайлов, а если не помогло — в + деградацию на CPU. +- **4 ГБ VRAM — это про `PHOTO_AI_LOAD_ALL` и `PHOTO_AI_TILE`, а не про лестницу.** `x2plus` и `gfpgan` + влезают (свободно ~100–300 МБ), `general-x4v3` втрое тяжелее. Проверено: после `x4v3 face=all` + поднимается `tile=2048`, но дефолтные 256 проходят без единого предупреждения. +- **`tile` по-прежнему не залипает** — после OOM в `finally` восстанавливается `PHOTO_AI_TILE`, и + `degrade_to_cpu` больше не сбрасывает операторское значение. + +--- + +### Журнал GPU-прогона (закрывает строку «GPU-пуск» раздела 2) + +Стоп-условие снято: `nvidia-ctk` и спека `/etc/cdi/nvidia.yaml` появились на хосте, поэтому GPU-оверрейд +переведён с классического резервирования (`driver: nvidia, count: 1`) на CDI (`device_ids: +nvidia.com/gpu=all`) — так демон перезапускать не нужно, а `/etc/docker/daemon.json` на хосте нет. + +| Проверка | Как | Результат | +|---|---|---| +| Сборка GPU-образа | `docker compose -f docker-compose.yml -f docker-compose.gpu.yml build photo-ai` | `whatido-photo-ai:cu126`, 12 ГБ; `torch 2.14.0+cu126`, `cuda: 12.6` — те же версии, что и в CPU-образе | +| Старт | оверрайд + `up -d photo-ai` | `healthy`, в лог `устройство: cuda:0 (NVIDIA GeForce RTX 3050 Laptop GPU), half=True` | +| `/health` | curl | `device: cuda:0`, `half: true`, `driver: 615.71.09`, `cuda: 12.6`, `vram_total_mb: 3822`, `ready: true` | +| Обратная совместимость | базовый `docker compose up -d photo-ai` без оверрайда | сервис возвращается на CPU, `PHOTO_AI_DEVICE=auto` | + +Решения, которые нужно знать дальше: + +- **`TORCH_VARIANT` в оверрайде захардкожен (`cu126`), а не берётся из `.env`:** `TORCH_VARIANT=cpu` + в `.env` иначе молча собрал бы GPU-конфигурацию без CUDA. Причина смены `cu124 → cu126`: в индексе + `cu124` последний torch — `2.6.0`, а `cu126` даёт ровно `2.14.0`/`0.29.0`, как CPU-образ. +- **GPU-образ тегируется отдельно** (`whatido-photo-ai:cu126`), поэтому сборка GPU-варианта не + перетирает `whatido-photo-ai:latest` и откат на CPU — обычный `docker compose up -d photo-ai`. +- **Ветка CUDA в плане остаётся непроверенной ровно в одном месте** — печать realesных GPU-байтов: + на CPU/CUDA результаты x2 совпадают побайтно, но `half=True` на CUDA считает в fp16, поэтому + байты CPU и GPU для одной картинки не равны (это не регресс I1: эталон снимается на CPU). + --- ## 7. Stage 4. `worker.js` + `server.js` diff --git a/docker-compose.gpu.yml b/docker-compose.gpu.yml index 34e9883..67ecab6 100644 --- a/docker-compose.gpu.yml +++ b/docker-compose.gpu.yml @@ -2,22 +2,31 @@ # Использование: # 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. +# Требуется драйвер NVIDIA и NVIDIA Container Toolkit. GPU выдаётся контейнеру через CDI +# (device_ids ниже соответствует спеку /etc/cdi/nvidia.yaml): этот путь не требует правки +# /etc/docker/daemon.json и перезапуска демона. Если nvidia-runtime зарегистрирован в демоне +# (nvidia-ctk runtime configure --runtime=docker), вместо CDI можно вернуть классический вид +# резервирования: driver: nvidia, count: 1 — результат тот же. +# TORCH_VARIANT=cu126, а не cu124: в индексе cu124 последний torch — 2.6.0, а cu126 даёт ровно +# те же torch 2.14.0 / torchvision 0.29.0, что и CPU-образ, поэтому варианты сборки отличаются +# только CUDA-библиотеками. +# Образ тегируется отдельно (whatido-photo-ai:cu126), чтобы сборка GPU-варианта не перетирала +# CPU-образ whatido-photo-ai:latest. +# PHOTO_AI_DEVICE=cuda при недоступной CUDA не роняет сервис: app.py пишет WARN и работает +# на CPU, /health при этом отвечает 200. services: photo-ai: + image: whatido-photo-ai:cu126 build: args: - TORCH_VARIANT: cu124 + TORCH_VARIANT: cu126 environment: PHOTO_AI_DEVICE: ${PHOTO_AI_DEVICE:-cuda} deploy: resources: reservations: devices: - - driver: nvidia - count: 1 + - driver: cdi + device_ids: + - nvidia.com/gpu=all capabilities: [gpu] diff --git a/photo-ai/app.py b/photo-ai/app.py index 45e95e2..34e934e 100644 --- a/photo-ai/app.py +++ b/photo-ai/app.py @@ -38,6 +38,7 @@ FACE_MODES = ('off', 'face', 'all') DEFAULT_MODEL = 'x2plus' DEFAULT_STRENGTH = 0.7 MIN_TILE = 64 +MIN_FACE_SIDE = 320 POOL_LIMIT = 2 WARMUP_SIZE = 64 RETRY_AFTER_SEC = 5 @@ -233,19 +234,41 @@ class ModelNotReady(EnhanceError): self.label = label +class TileOOM(Exception): + pass + + def error_response(status_code, message, headers=None): return JSONResponse(status_code=status_code, content={'ok': False, 'error': message}, headers=headers) def is_oom(err): - if isinstance(err, torch.cuda.OutOfMemoryError): + if isinstance(err, (torch.cuda.OutOfMemoryError, TileOOM)): return True text = str(err).lower() return any(mark in text for mark in ('out of memory', 'not enough memory', 'alloc_cpu', "can't allocate memory")) +def guard_forward(model): + if getattr(model, '_photo_ai_guarded', False): + return model + forward = model.forward + + def guarded(*args, **kwargs): + try: + return forward(*args, **kwargs) + except RuntimeError as err: + if not is_oom(err): + raise + raise TileOOM(str(err)) from err + + model.forward = guarded + model._photo_ai_guarded = True + return model + + def tile_ladder(base): if base <= 0: return [base] @@ -459,7 +482,7 @@ def build_esrgan(name, denoise): scale=spec['scale'], model_path=model_path, dni_weight=dni_weight, - model=spec['arch'](), + model=guard_forward(spec['arch']()), tile=state['tile'], tile_pad=TILE_PAD, pre_pad=PRE_PAD, @@ -511,6 +534,7 @@ def build_face_gfpgan(spec, outscale, bg): bg_upsampler=bg.upsampler, device=torch.device(state['device']), ) + guard_forward(restorer.gfpgan) def restore(img, strength): cropped, _restored, output = restorer.enhance(img, has_aligned=False, only_center_face=False, @@ -532,6 +556,7 @@ def build_face_codeformer(spec, outscale, bg): connect_list=['32', '64', '128', '256'], device=state['device'], fp16=False) net.load_state_dict(torch.load(path, map_location=lambda storage, loc: storage)) net.eval() + guard_forward(net) helper = face_helper(outscale) def restore(img, strength): @@ -612,7 +637,7 @@ def run_guarded(img, outscale, model_name, face, face_name, strength, warnings): pool.set_tile(tile) try: return process_image(img, outscale, model_name, face, face_name, strength) - except RuntimeError as err: + except (RuntimeError, TileOOM) as err: if not is_oom(err): raise EnhanceError('ошибка модели: %s' % err, 500) from err log.warning('нехватка памяти при tile=%s: %s', tile, err) @@ -621,7 +646,7 @@ def run_guarded(img, outscale, model_name, face, face_name, strength, warnings): degrade_to_cpu(warnings) try: return process_image(img, outscale, model_name, face, face_name, strength) - except RuntimeError as err: + except (RuntimeError, TileOOM) as err: if is_oom(err): raise EnhanceError('не хватило памяти даже на CPU: %s' % err, 500) from err raise EnhanceError('ошибка модели на CPU: %s' % err, 500) from err @@ -819,6 +844,12 @@ async def enhance(request: Request, outscale = min(max(int(scale), 2), 4) fmt = output_format(image.filename) warnings = [] + if face_mode != 'off': + if min(img.shape[0], img.shape[1]) < MIN_FACE_SIDE: + warnings.append('вход меньше %d×%d — лица могут не найтись' + % (MIN_FACE_SIDE, MIN_FACE_SIDE)) + if face_name == 'codeformer' and not state['device'].startswith('cuda'): + warnings.append('CodeFormer на %s медленнее GFPGAN' % state['device']) output, faces_found = await asyncio.to_thread(run_guarded, img, outscale, model_name, face_mode, face_name, float(strength), warnings) payload, media_type = encode_image(output, fmt, int(quality))