# План: Улучшение лиц на фотографиях (Real-ESRGAN + GFPGAN/CodeFormer) с автовыбором GPU/CPU Статус: **план, реализация не начата**. Документ описывает целевую архитектуру, изменения по файлам, порядок внедрения и критерии приёмки. Реализацию начинать после знакомства с этим файлом. --- ## 1. Постановка задачи Сейчас система улучшает фотографии одной моделью `RealESRGAN_x2plus` (x2, CPU, `half=False`, `device='cpu'`, захардкожено в `photo-ai/app.py:44`). Модель хорошо восстанавливает текстуры (одежда, фон, бумага), но **лица** при апскейле часто получают артефакты, «пластиковую» кожу и искажённые черты, потому что у `RealESRGAN_x2plus` нет приора на структуру лица. Нужно: 1. Добавить модели восстановления **лиц** — GFPGAN v1.4 и/или CodeFormer (оба — face restoration с prior-сетью, работают в связке с `RealESRGANer` как `bg_upsampler`). 2. Дать пользователю **выбор модели** для обработки: универсальная x2, face-модель, комбинация (фон + лица), быстрая VGG-модель `realesr-general-x4v3`. 3. Поддержать работу **на GPU и на CPU** с **автоматическим выбором** устройства: есть рабочий CUDA — используем GPU, нет — молча и без падений уходим на CPU. 4. Не допустить простоя GPU-контейнера: пока модели грузятся — сервис отвечает `503`, воркер ждёт, а не теряет задания. 5. Не сломать существующие контракты: `photo_jobs`, `/enhance`, `PHOTO_AI_URL`, автономную работу без `photo-ai` (`PHOTO_AI_URL` пустой → кнопка «ИИ» недоступна). ### Ограничения окружения (проверено на текущем хосте) | Параметр | Значение | Следствие | |----------|----------|-----------| | GPU | `NVIDIA GeForce RTX 3050 ...`, 4096 MiB VRAM, драйвер 615.71.09, CUDA UMD 13.4 | GPU-режим реален, но 4 ГБ VRAM — тесно, нужен `tile` и запас | | `/dev/dri` | `card1`, `card2`, `renderD128`, `renderD129` | iGPU тоже виден, но torch будет использовать CUDA | | `nvidia-ctk` | **не установлен** | нужен NVIDIA Container Toolkit на хосте, иначе `--gpus` не заработает | | Runtime Docker | только `runc` (нет `nvidia`) | требуется установка toolkit + `docker compose` override | | CPU | Intel i5-12500H, 16 потоков | CPU-режим приемлем как fallback, но медленный | | RAM | 15 GiB (занято ~9 ГБ), swap 31 GiB | CPU-модели + torch требуют ~2–3 ГБ; следить за OOM | | Диск | 59 ГБ свободно на `/home` | веса: x2plus 64 МБ + GFPGAN 333 МБ + CodeFormer 360 МБ + wdn 64 МБ — ок | | Docker / Compose | 29.8.1 / 5.5.1 | поддерживают `deploy.resources.reservations.devices` (Compose v5) | Важно: **GPU-режим не должен быть обязательным условием запуска**. Если toolkit не установлен, compose-файл с `devices` не поднимется — поэтому GPU выносим в отдельный override-файл. --- ## 2. Что уже есть (точки интеграции) | Место | Что делает | Что меняем | |-------|-----------|-----------| | `photo-ai/app.py` | FastAPI, `POST /enhance` (`image`, `scale`), одна модель, `device='cpu'` | Расширяем до реестра моделей + автовыбор device + `POST /enhance` с `model`/`face` | | `photo-ai/Dockerfile` | `python:3.10-slim`, torch CPU-only, правка `basicsr/data/degradations.py` | Разделяем на CPU-базу и GPU-базу (`ARG`), добавляем `gfpgan`, `facexlib` | | `docker-compose.yml` (`photo-ai`) | build `./photo-ai`, `MODEL_PATH`, `MAX_INPUT_PIXELS`, том `photo-ai-models` | Добавляем env `PHOTO_AI_DEVICE`, `PHOTO_AI_FACE_MODEL`, `PHOTO_AI_TILE`, healthcheck | | `worker.js` → `createPhotoEnhanceWorker` | `runAiEnhance(srcKey)` шлёт `image` + `scale=2`, ждёт `image/jpeg` (таймаут 300 с, 3 попытки) | Передаём `model`/`face`/`strength` из `job.params`, разбираем JSON-ответ, 503 ждёт без траты попыток | | `server.js` → `POST /api/entries/:id/photo/enhance-ai` | `INSERT INTO photo_jobs (entry_id, action, status, params) VALUES ($1,'ai','pending',NULL)` | Принимаем `model`/`face`/`face_model`/`strength`, валидируем, пишем в `params` (колонка уже есть) | | `server.js` → `GET /api/photo-jobs/status` | `ai_configured`, `ai_url`, `worker`, `counts` | Добавляем `service.device`, `service.models`, `service.face_models`, `service.ready` | | `server.js` → `PHOTO_JOB_ACTIONS` | белый список действий, `action VARCHAR(20)` | Новые действия `ai_face`, `ai_upscale` в двух местах (валидация + restore) | | `public/js/journal.js` (кнопка «🤖 ИИ») | Ставит задание `action='ai'` | Выбор модели: универсальная / быстрая / лица / обе | | `public/js/worker.js` + `worker.html` | Таблица заданий, `PHOTO_ACTION_LABELS`, сравнение «Было/Стало» | Новые метки, показ модели/устройства/времени обработки | | `settings.html` / `settings.js` | `photo_enhance_engine` (`auto`/`server`/`client`) | Новые настройки: `photo_ai_face_mode`, `photo_ai_device_pref` | Текущее поведение, которое **сохраняем байт-в-байт**: `/enhance` с `scale=2` без указания модели → та же картинка, что и сегодня (x2plus, JPEG q92). Это нужно, чтобы старые `photo_jobs` со `params = NULL` и все закешированные превью не изменились. --- ## 3. Модели: что именно добавляем ### 3.1 Каталог моделей | Ключ | Класс | Веса | Размер | Назначение | |------|-------|------|--------|-----------| | `x2plus` | `RRDBNet(scale=2)` | `RealESRGAN_x2plus.pth` | 64 МБ | **текущая**, универсальный апскейл x2, дефолт | | `general-x4v3` | `SRVGGNetCompact(upscale=4)` | `realesr-general-x4v3.pth` | 4.7 МБ | быстрый апскейл x4 + денойз через DNI (`realesr-general-wdn-x4v3.pth`) | | `animevideo-v3` | `SRVGGNetCompact(upscale=4)` | `realesr-animevideov3.pth` | 2.4 МБ | быстрый x4, для скриншотов/иллюстраций | | `gfpgan` | `GFPGANer(arch='clean', channel_multiplier=2)` | `GFPGANv1.4.pth` | 333 МБ | **восстановление лиц**, `bg_upsampler` = выбранный ESRGAN | | `codeformer` | `CodeFormer` | `codeformer.pth` + `detection_Resnet50_Final.pth` + `parsing_parsenet.pth` | 360 МБ + ~110 МБ | **восстановление лиц**, регулируемая сила `fidelity_weight` (`-w`), лучше на сильных искажениях | Рекомендация: **GFPGAN v1.4 как основная face-модель** (меньше весов, стабильнее на 4 ГБ VRAM, это модель по умолчанию в апстриме `inference_realesrgan.py` через `--face_enhance`), CodeFormer — опционально, как альтернатива с регулируемой силой. На CPU CodeFormer практически неработоспособен по времени (facexlib + parsing) — оставляем его «только GPU, если включён явно». ### 3.2 Режимы обработки (`face_mode`) | `face_mode` | Что происходит | Модель | Когда использовать | |-------------|----------------|--------|--------------------| | `off` | только апскейл фона, лицо не трогается | ESRGAN | текущее поведение, фон/текстуры | | `face` | апскейл фона + **только лица** вставлены восстановленными (paste-back) | ESRGAN + GFPGAN/CodeFormer | портреты, крупные лица | | `all` | как `face`, плюс мягкий денойз фона | ESRGAN(+wdn) + GFPGAN | зашумлённые снимки с веб-камеры 640×480 | Технически GFPGAN работает так: детектирует лица (facexlib/RetinaFace), кропает → восстанавливает → paste-back в альбом. Если лиц не найдено — возвращает чистый результат `bg_upsampler`: это безопасный no-op и не должно считаться ошибкой. ### 3.3 Почему face-модель нельзя ставить «всегда» 1. **Скорость.** Детекция + face-restore + paste-back даёт +40…150 % ко времени. На CPU это разница между ~40 с и ~90 с на фото 640×480. 2. **Гарантий нет.** GFPGAN «дорисовывает» лица по приору: на сильно замытых, боковых или закрытых лицах он может сделать человека похожим на другого. Для журнала посещаемости ошибка идентификации недопустима, поэтому face-режим — **явный выбор пользователя**, а не молчаливый дефолт. 3. **VRAM.** GFPGAN + x2plus одновременно держат два графа на GPU; на 4 ГБ нужен `tile <= 256`. ### 3.4 Точные URL весов (скачиваются при первом запуске) ``` https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-general-x4v3.pth https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-general-wdn-x4v3.pth https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-animevideov3.pth https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.4.pth https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth ``` Каждый файл кладём в `/models/weights/.pth` (том `photo-ai-models`), загрузка через `.tmp` + `os.replace` и проверку минимального размера — иначе оборванная закачка оставит «битые» веса, и сервис будет падать при загрузке. --- ## 4. Автоматический выбор GPU/CPU ### 4.1 Приоритет выбора (в `app.py` при старте) ``` 1. PHOTO_AI_DEVICE=cuda|cpu|auto (env, дефолт auto) 2. если auto: a. torch.cuda.is_available() and torch.cuda.device_count() > 0 -> device = 'cuda:0', half = True (fp16 быстрее и экономит VRAM) b. иначе если torch.backends.mps.is_available() -> device = 'mps', half = False (Apple Silicon, на случай dev-машины) c. иначе -> device = 'cpu', half = False 3. если явно cuda, но cuda недоступна -> НЕ падать: WARN в лог и уйти на cpu (контейнер обязан подниматься даже без GPU — требование отказоустойчивости) 4. явно cpu — всегда cpu, даже если GPU есть (для отладки и воспроизводимости) ``` ### 4.2 Прогрев, ленивая загрузка и деградация - `load_model()` вызывается в `startup` (сейчас через `asyncio.to_thread`), модели грузятся **лениво по требованию** и кешируются в `MODELS` dict под `threading.Lock`. Первый запрос к новой модели оплачивает её загрузку (x2plus 64 МБ грузится быстрее, чем GFPGAN 333 МБ). - Пока нужная модель не готова, `/enhance` возвращает `503` + `Retry-After: 5`, а `/health` отдаёт `{"ok": true, "ready": false, "loading": ["gfpgan"]}`. Воркер на `503` **не** тратит попытку из `PHOTO_MAX_ATTEMPTS`: он ждёт и повторяет (см. §5.4). - `CUDA out of memory` при инференсе — ловим `RuntimeError`, уменьшаем `tile` вдвое (256 → 128 → 64) и повторяем **один раз**; если снова OOM — переключаемся на `cpu`, инвалидируем модель и повторяем. Если и на CPU не получилось — `500` с понятным текстом. - Прогрев (warm-up) на синтетическом шуме 64×64 сразу после загрузки: убирает «первый запрос в 3 раза дольше» и немедленно выявляет OOM. ### 4.3 Что показывать оператору `GET /health` (расширяем, обратно совместимо: поле `ok` остаётся): ```json { "ok": true, "ready": true, "device": "cuda:0", "device_name": "NVIDIA GeForce RTX 3050 ...", "half": true, "tile": 256, "driver": "615.71.09", "cuda": "13.4", "vram_total_mb": 4096, "vram_free_mb": 3780, "models": ["x2plus", "general-x4v3", "animevideo-v3"], "face_models": ["gfpgan", "codeformer"], "loaded": ["x2plus", "gfpgan"], "max_pixels": 4000000 } ``` `server.js` проксирует это в `GET /api/photo-jobs/status` → `service` и в блок «Статус стека» (`getStackInfo()`, `public/settings.html`), чтобы было видно, на чём реально считает фото-ИИ. ### 4.4 Docker: как отдать GPU контейнеру Два обязательных шага на хосте (сейчас **не выполнены** — `nvidia-ctk` отсутствует): ```bash # 1. NVIDIA Container Toolkit 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 sudo nvidia-ctk runtime configure --runtime=docker && sudo systemctl restart docker # 2. Проверка docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi ``` Compose делим на два файла, чтобы GPU был **опцией, а не требованием** (по образцу существующего `docker-compose.minio.yml`): - `docker-compose.yml` — `photo-ai` без GPU (CPU-образ, как сейчас). Стек поднимается на любой машине. - `docker-compose.gpu.yml` (новый) — override: ```yaml services: photo-ai: build: context: ./photo-ai args: TORCH_VARIANT: cu124 environment: PHOTO_AI_DEVICE: cuda deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] ``` Запуск GPU-режима: ```bash docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build photo-ai ``` Если `photo-ai` уже запущен в CPU-режиме — сначала `docker compose stop photo-ai`, затем команда выше (иначе Compose ругается на конфликт конфигурации сервиса). --- ## 5. Изменения по файлам ### 5.1 `photo-ai/app.py` (переписываем, ~250–300 строк) Новая структура: ``` ENV: PHOTO_AI_DEVICE, PHOTO_AI_FACE_MODEL, PHOTO_AI_TILE, PHOTO_AI_MAX_PIXELS, PHOTO_AI_MODELS_DIR, PHOTO_AI_LOAD_ALL, PHOTO_AI_WARMUP, PHOTO_AI_JPEG_QUALITY pick_device() -> (device, half, device_name) # §4.1 MODEL_REGISTRY = { 'x2plus': {...}, 'general-x4v3': {...}, 'animevideo-v3': {...} } FACE_REGISTRY = { 'gfpgan': {...}, 'codeformer': {...} } class ModelPool: # Lock, dict, lazy load, OOM-ретрай get_esrgan(name) -> RealESRGANer get_face(name, bg_upsampler) -> GFPGANer | CodeFormer encode_jpeg(out, quality) @app.get('/health') # расширенный контракт §4.3 @app.get('/models') # список моделей + устройство @app.post('/enhance') # image, scale, model, face, face_model, strength ``` Ключевые детали реализации: - `POST /enhance` принимает те же `image` и `scale`, плюс новые необязательные поля: `model` (дефолт `x2plus`), `face` (`off`|`face`|`all`, дефолт `off`), `face_model` (`gfpgan`|`codeformer`), `strength` (0.0–1.0, только CodeFormer, дефолт 0.7), `jpeg_quality` (70–100, дефолт 92). Неизвестная модель → `400` со списком допустимых. - Полная обратная совместимость: без `model`/`face` → путь «x2plus, JPEG q92», как сегодня. - `face != off` → `face_enhancer.enhance(img, has_aligned=False, only_center_face=False, paste_back=True)`; в ответ добавляем `faces_found`. - **Два формата ответа**: JSON (`{ok, image_base64, model, face, face_model, faces_found, device, elapsed_ms, warnings}`) при `Accept: application/json` — новый путь для воркера; сырой `image/jpeg` без заголовка — текущее поведение (совместимость с ручными `curl`). - `upsampler.enhance()` уже делает тайлинг сам — `tile`/`tile_pad=10` остаются, `tile` берём из env. - `half=True` только при CUDA; `dni_weight` — только для `general-x4v3` при `denoise_strength != 1`. - Расширения определяем по имени файла, а не по `UploadFile.content_type` (браузер и `FormData` в `worker.js` шлют `image/jpeg` для любого исходника — сейчас это уже так, сохраняем). ### 5.2 `photo-ai/Dockerfile` ```dockerfile FROM python:3.10-slim ARG TORCH_VARIANT=cpu # cpu | cu124 ARG TORCH_INDEX=https://download.pytorch.org/whl/${TORCH_VARIANT} ``` - `TORCH_VARIANT=cpu` → `torch torchvision --index-url .../whl/cpu` (как сейчас); `cu124` → тот же `pip` с `.../whl/cu124`. Образ один, вариант — аргумент сборки. - Добавить `gfpgan==1.3.8` и `facexlib==0.3.0` (CodeFormer — из TencentARC, пакет/вендоринг). **Проверить доступность пакетов в зеркале pip заранее** — это риск сборки, если недоступны, вендорим исходники в `photo-ai/vendor/` и копируем каталог в образ. - `libgl1 libglib2.0-0` уже есть — facexlib их требует. - Патч `basicsr/data/degradations.py` (`torchvision.transforms.functional_tensor` → `functional`) **сохранить** — без него basicsr падает на torch >= 2.0. - `ENV PHOTO_AI_MODELS_DIR=/models`; старый `MODEL_PATH` продолжаем читать как алиас, чтобы существующий `.env`/том не сломался. - Размер образа: CPU ~1.2 ГБ → ~2.5 ГБ; cu124 ~6–7 ГБ. Учитывать при `--build`. - Тома: `photo-ai-models` уже есть — все веса в `/models/weights/*.pth`. ### 5.3 `docker-compose.yml` ```yaml photo-ai: environment: PHOTO_AI_DEVICE: ${PHOTO_AI_DEVICE:-auto} PHOTO_AI_FACE_MODEL: ${PHOTO_AI_FACE_MODEL:-gfpgan} PHOTO_AI_TILE: ${PHOTO_AI_TILE:-256} PHOTO_AI_MAX_PIXELS: ${PHOTO_AI_MAX_PIXELS:-4000000} PHOTO_AI_LOAD_ALL: ${PHOTO_AI_LOAD_ALL:-0} PHOTO_AI_JPEG_QUALITY: ${PHOTO_AI_JPEG_QUALITY:-92} MODEL_PATH: /models/RealESRGAN_x2plus.pth healthcheck: test: ["CMD-SHELL", "python -c \"import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3).status==200 else 1)\""] interval: 30s timeout: 5s retries: 5 start_period: 300s ``` `start_period: 300s` — загрузка x2plus + GFPGAN на CPU занимает минуты; короткий период даст `unhealthy` на старте. `PHOTO_AI_LOAD_ALL=1` предзагружает все модели (для GPU-сервера с запасом RAM), по умолчанию `0` — ленивая загрузка. ### 5.4 `worker.js` (функция `createPhotoEnhanceWorker`) - `runAiEnhance(srcKey, params)`: - читает `params.model`, `params.face`, `params.face_model`, `params.strength`; - отправляет их вместе с `image` и `scale`; ставит `Accept: application/json`, разбирает JSON-ответ, декодирует base64 в буфер; - если сервис вернул `image/jpeg` (старая версия photo-ai) — работает как сейчас, без ошибок; - `503` (`Retry-After`) обрабатывает отдельно: `sleep` 10 с, `return false` — **без** инкремента `attempts` (мягкий повтор, задание не сгорает); - `AbortError` от `AbortSignal.timeout(PHOTO_AI_TIMEOUT_MS)` → сообщение «ИИ-сервис не ответил за N с» (уже ошибка с попытками); - в аудит пишет модель/устройство/время: `logAudit(null, 'photo.job.preview', { ..., model, face, device, elapsed_ms })`. - `processOne`: `job.action === 'ai' || job.action === 'ai_face'` → AI-путь; `enhance` → sharp. Если `params` пусты, `action='ai_face'` даёт дефолт `face='face'`. - `CONFIG` воркера дополняем `face_model`, `default_model`, `face_timeout_ms` — они попадают в `GET /api/photo-jobs/status.worker.config`. - Для face-режима на CPU вводим отдельный, больший таймаут: `PHOTO_AI_FACE_TIMEOUT_MS` (дефолт 600 000) вместо 300 000. ### 5.5 `server.js` 1. `POST /api/entries/:id/photo/enhance-ai` (строка 5114) — тело `{ model, face, face_model, strength }`: - `model` ∈ `['x2plus', 'general-x4v3', 'animevideo-v3']`; - `face` ∈ `['off', 'face', 'all']`; - `face_model` ∈ `['gfpgan', 'codeformer']`; - `strength` — число 0…1, только для CodeFormer; - `params = JSON.stringify({ model, face, face_model, strength })` в `photo_jobs.params`; - `action = face === 'off' ? 'ai' : 'ai_face'`; - `400` при невалидных значениях; пустое тело → сегодняшнее поведение (`params = NULL`, `action = 'ai'`). Валидация — инлайн-хелперами (`optInt`) и явными списками, как в `PUT /api/settings`. 2. `PHOTO_JOB_ACTIONS` — добавить `'ai_face'`, `'ai_upscale'`. Схема не меняется (`action VARCHAR(20)`), но белый список используется в двух местах: `ensurePhotoJobsTable()` (~1238–1256) и при разборе restore-данных (~2259) — обновить **оба**. 3. `GET /api/photo-jobs/status` (строка 5782) — прокинуть `service.device`, `service.device_name`, `service.ready`, `service.vram_free_mb`, `service.models`, `service.face_models` из `GET /health` photo-ai (таймаут 5 с, по образцу `aiHealthCheck()`). Запрос делать без падения: photo-ai недоступен → `service = { reachable: false }`. 4. Новый `GET /api/photo-ai/health` (`requireAdmin`) — прямой прокси `/health` для оператора. В `getStackInfo()` добавить блок `photo_ai` (`device`, `device_name`, `vram_total_mb`, `models`) — рендерится в «Статус стека» на `public/settings.html`. 5. Настройки — валидация рядом со строкой 1710: - `photo_ai_face_mode` ∈ `['off', 'face', 'all']`, дефолт `off`; - `photo_ai_device_pref` ∈ `['auto', 'cuda', 'cpu']`, дефолт `auto` — **только для UI и документации**: фактическое устройство определяет контейнер через env, UI показывает, совпадает ли желаемое с фактическим (если нет — подсветить). - дефолты дописать в `db/init.sql`, `db/migration.sql` (`INSERT ... ON CONFLICT DO NOTHING`) и в `keys`/`defaults` `/api/public-settings` (строка 1663–1664); - `photo_ai_enabled` там уже отдаётся (строка 1677) — сохранить. 6. `worker.js` вызывается с новыми параметрами: в `createPhotoEnhanceWorker({...})` (строка 6045) добавить `faceTimeoutMs: PHOTO_AI_FACE_TIMEOUT_MS`, `defaultFaceModel: PHOTO_AI_FACE_MODEL`. 7. Предупреждения сервиса (`warnings`, например «вход меньше 320×320») сохранять в аудит, чтобы оператор понимал, что face-режим не сработал не из-за ошибки. ### 5.6 Фронтенд - `public/js/journal.js` (кнопка «🤖 ИИ», ~строка 620) — рядом dropdown: «Универсально (x2)», «Быстро (x4)», «Лица (GFPGAN)», «Лица + фон». Значение уходит в `POST /api/entries/:id/photo/enhance-ai` телом `{ model, face, face_model }`. Дефолт — из `photo_ai_face_mode` (публичные настройки уже читаются на этой странице). - `public/js/worker.js` — `PHOTO_ACTION_LABELS` дополнить (`ai_face`: «ИИ + лица», `ai_upscale`: «ИИ-апскейл»); в модалке сравнения «Было/Стало» показать `model`, `device`, `faces_found`, `elapsed_ms`. Новых колонок в БД не нужно — данные берём из `params` и аудита. - `public/settings.html` + `public/js/settings.js` — блок «Фото-ИИ»: селект face-режима, селект устройства («желаемое» + строка «фактическое»), read-only статус (`device_name`, VRAM, список моделей) из `/api/photo-jobs/status`. Новые поля добавить в `DIRTY_FIELDS` (строка 1 `settings.js`) и в сборку payload (строка ~739). - `public/js/audit.js` — новые коды действий аудита прописать в словарь меток, иначе в UI будет сырой код (`photo.job.preview` уже есть, при добавлении `photo.job.ai` — добавить и там). ### 5.7 Документация и тесты - `AGENTS.md` — раздел про photo-ai: реестр моделей, device-политика, новые env, GPU-override, обновлённый контракт `/enhance` и `/health`. - `README.md` — установка NVIDIA Container Toolkit, команда запуска с `docker-compose.gpu.yml`, проверка `GET /api/photo-jobs/status` → `service.device`. - `.env.example` — новые переменные с комментариями (см. §7). - `api.smoketest.js` — добавить проверки: - `POST /api/entries/:id/photo/enhance-ai` с `model: 'нет такой'` → `400`; - `face: 'face'` без `PHOTO_AI_URL` → `503`; - `GET /api/photo-jobs/status` содержит `service` (или `ai_configured: false`). Это соответствует правилу из `AGENTS.md`: контракт авторизации и API фиксируется в smoke-тесте в том же изменении. --- ## 6. Порядок внедрения (этапы) Каждый этап заканчивается проверяемым результатом и не ломает предыдущий. ### Этап 0. Подготовка (0.5 дня) - Установить NVIDIA Container Toolkit, проверить `docker run --gpus all ... nvidia-smi`. - Зафиксировать текущее состояние `.env` (`PHOTO_AI_URL=` пусто или `http://photo-ai:8080`). - Сохранить эталон «до»: 3–5 фото (портрет, групповое, без лиц, зашумлённое), прогнать `curl -F image=@photo.jpg -F scale=2 http://localhost:8080/enhance`. **Приёмка:** `nvidia-smi` внутри контейнера видит RTX 3050; эталонные JPEG сохранены для сравнения на следующих этапах. ### Этап 1. `app.py`: реестр моделей + автовыбор device (1–1.5 дня) - `pick_device()`, `ModelPool`, расширенный `/health`, ленивая загрузка x2plus, warm-up. - Старый `/enhance` по поведению не меняется (проверить визуально и по размеру файла). **Приёмка:** `/health` отдаёт `device`, `device_name`, `half`; `PHOTO_AI_DEVICE=cpu` при рабочей CUDA даёт `cpu`; `PHOTO_AI_DEVICE=cuda` без toolkit даёт `cpu` + WARN, сервис поднимается. ### Этап 2. Dockerfile + compose (0.5–1 день) - `ARG TORCH_VARIANT`, `docker-compose.gpu.yml`, healthcheck, новые env, алиас `MODEL_PATH`. **Приёмка:** CPU- и GPU-сборка стартуют; `/api/photo-jobs/status` показывает `service.device = "cuda:0"` в GPU-режиме и `"cpu"` в CPU-режиме; при пустом `PHOTO_AI_URL` приложение полностью работает без photo-ai. ### Этап 3. GFPGAN — face-режим (1–2 дня) - `FACE_REGISTRY`, поля `face`/`face_model`/`strength` в `/enhance`, JSON-ответ, `faces_found`, OOM-ретрай и fallback на CPU. **Приёмка:** на тестовом портрете 640×480 в режиме `face` лицо резче, `faces_found = 1`; на фото без лиц — `faces_found = 0` и результат не хуже, чем `x2plus`; искусственный OOM (`tile=1024` на 4 ГБ) деградирует до CPU без падения сервиса. ### Этап 4. Воркер и API (1 день) - `worker.js`: `params` → модель/face, JSON-разбор, `503`-ожидание без траты попыток, аудит. - `server.js`: валидация `params`, `action = 'ai_face'`, `service` в статусе, новые настройки. **Приёмка:** задание через API с `face='face'` доходит до `done`, параметры сохранены в `photo_jobs.params`, в аудите — модель/устройство/время; остановка photo-ai на лету даёт задание, которое дообработается после возврата сервиса (без `error`). ### Этап 5. Фронтенд (1 день) - Выбор модели в журнале, статус устройства в настройках, новые метки в воркере. **Приёмка:** из журнала доступны все 4 варианта; после постановки видно «В очереди», затем сравнение «Было/Стало»; в настройках показано фактическое устройство и VRAM. ### Этап 6. Docs + smoke (0.5 дня) - `AGENTS.md`, `README.md`, `.env.example`, `api.smoketest.js`. **Приёмка:** `node api.smoketest.js` проходит; тесты валидации возвращают `400`; документация совпадает с кодом. **Итого:** ~5–7 рабочих дней. Этапы 1–3 дают ценность уже без фронтенда (ручные вызовы `curl`). --- ## 7. Новые переменные окружения ```bash # === Фото-ИИ (Real-ESRGAN + восстановление лиц) === # Адрес сервиса; пусто = фото-ИИ выключен (кнопка «ИИ» недоступна) PHOTO_AI_URL= # Максимум входных пикселей (даунскейл перед обработкой) PHOTO_AI_MAX_PIXELS=4000000 # Устройство: auto | cuda | cpu. auto = CUDA, если доступна, иначе CPU PHOTO_AI_DEVICE=auto # Тайл для апскейла: меньше = меньше VRAM, но медленнее (256 на 4 ГБ, 512+ при 8 ГБ+) PHOTO_AI_TILE=256 # Модель лиц: gfpgan | codeformer | none PHOTO_AI_FACE_MODEL=gfpgan # Предзагружать все модели при старте (1 — да, нужно больше RAM/VRAM) PHOTO_AI_LOAD_ALL=0 # Режим лиц по умолчанию для UI: off | face | all PHOTO_AI_FACE_MODE=off # Качество JPEG результата (70–100) PHOTO_AI_JPEG_QUALITY=92 # Отдельный таймаут для face-режима, мс (на CPU медленно) PHOTO_AI_FACE_TIMEOUT_MS=600000 ``` --- ## 8. Риски и как их закрываем | Риск | Вероятность | Митигация | |------|-------------|-----------| | `nvidia-container-toolkit` не установлен / политика хоста запрещает | средняя | GPU — отдельный override-файл; CPU-путь остаётся дефолтом и полностью рабочим; в `/health` видно фактическое устройство | | 4 ГБ VRAM не хватает для GFPGAN + x2plus | высокая | `tile=256` по умолчанию, авто-снижение до 128/64 при OOM, `half=True` только на CUDA, при повторном OOM — fallback на CPU | | GFPGAN «портит» лица (артефакты идентичности) | средняя | face-режим только по явному выбору; результат попадает в `photo_jobs.after_path` и **не применяется автоматически** (нужно нажать «Применить»), история и «Вернуть оригинал» сохраняются | | Долгая загрузка весов (333 МБ) на первом запросе | высокая | ленивая загрузка + `503`/`Retry-After` вместо ошибки; `PHOTO_AI_LOAD_ALL=1` для прогрева; том `photo-ai-models` сохраняет веса между перезапусками; `start_period: 300s` в healthcheck | | Сборка ломается на `pip install gfpgan/facexlib` (пакета нет в зеркале) | средняя | **проверить доступность пакетов до начала работ**; при отсутствии — вендорить исходники в `photo-ai/vendor/` и копировать каталог в образ | | Рост образа до ~6–7 ГБ (cu124) | средняя | CPU-образ по умолчанию, GPU-образ собирается отдельно; на `/home` свободно 59 ГБ | | CPU-инференс с GFPGAN медленнее таймаута воркера | средняя | отдельный `PHOTO_AI_FACE_TIMEOUT_MS` (600 с) для face-режима; в UI предупреждение «на CPU медленно»; для слабых машин — `off` | | Регресс текущего `/enhance` | низкая | контракт по умолчанию не меняется; эталонное сравнение на этапах 1–2; старые `photo_jobs.params = NULL` продолжают работать | | Нехватка RAM (15 ГБ, занято ~9 ГБ) при загрузке моделей на CPU | средняя | `PHOTO_AI_LOAD_ALL=0`, кеш моделей с вытеснением (LRU, максимум 2), контроль через `docker stats` | | Воркер зацикливается на «вечно недоступном» сервисе | низкая | при `503`/`unreachable` — экспоненциальный `sleep`, лимит мягких повторов (например 60) → затем одна честная ошибка с понятным текстом | --- ## 9. Критерии готовности 1. `photo-ai` стартует и на CPU-хосте, и с CUDA, определяя устройство автоматически; `/health` сообщает фактическое устройство, имя GPU, VRAM, доступные и загруженные модели. 2. Существующий сценарий «🤖 ИИ» без параметров даёт результат, эквивалентный текущему. 3. Доступны режимы: универсальный x2, быстрый x4, только лица (GFPGAN), лица + фон. 4. Задание с face-моделью проходит полный цикл `pending → processing → done`; результат виден в сравнении «Было/Стало», применим вручную и откатывается. 5. Отсутствие GPU, отсутствие `nvidia-ctk` и недоступный `photo-ai` **не** ломают приложение: `PHOTO_AI_URL=` пусто → кнопка «ИИ» недоступна; сервис упал → задания пережидают (`503`), не сжигая попытки; пустой `.env` полностью совместим. 6. `node api.smoketest.js` проходит; `AGENTS.md`, `README.md`, `.env.example` описывают новые переменные и GPU-запуск. --- ## 10. Быстрые команды для проверки после реализации ```bash # Статус устройства и моделей docker compose exec -T app node -e "fetch(process.env.PHOTO_AI_URL+'/health').then(r=>r.json()).then(console.log)" curl -s -H "X-Auth-Token: $TOKEN" http://localhost:3003/api/photo-jobs/status | python3 -m json.tool # Прямой вызов с лицами (ручная проверка) curl -s -X POST http://localhost:8080/enhance \ -F image=@photo.jpg -F scale=2 -F model=x2plus -F face=face -F face_model=gfpgan \ -H 'Accept: application/json' | python3 -c "import json,sys,base64; d=json.load(sys.stdin); open('out.jpg','wb').write(base64.b64decode(d['image_base64'])); print({k:v for k,v in d.items() if k!='image_base64'})" # CPU-режим принудительно docker compose stop photo-ai PHOTO_AI_DEVICE=cpu docker compose -f docker-compose.yml up -d --build photo-ai # GPU-режим docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build photo-ai # Проверка обратной совместимости (без model/face — как раньше) curl -s -X POST http://localhost:8080/enhance -F image=@photo.jpg -F scale=2 -o old_behavior.jpg # Смоук node api.smoketest.js ```