diff --git a/.env.example b/.env.example index 121a4c1..ece3ac8 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,12 @@ WG_HANDSHAKE_TIMEOUT=60 # ИИ-улучшение фото (Real-ESRGAN), пусто = контейнер photo-ai PHOTO_AI_URL= PHOTO_AI_MAX_PIXELS=4000000 +# Сколько раз воркер повторит задание, если photo-ai недоступен (503/обрыв сети), +# не увеличивая attempts; после исчерпания — одна честная ошибка в задании. +PHOTO_AI_SOFT_MAX_RETRIES=60 +# Пауза перед мягким повтором и её потолок (мс), задержка растёт вдвое до потолка. +PHOTO_AI_SOFT_BACKOFF_MS=10000 +PHOTO_AI_SOFT_BACKOFF_MAX_MS=300000 # === Хранилище файлов (S3: SeaweedFS по умолчанию / MinIO) === # local — файлы в ./uploads (по умолчанию), s3 — объекты в бакете S3/MinIO. diff --git a/PLAN_PHOTO_FACE_AI.md b/PLAN_PHOTO_FACE_AI.md new file mode 100644 index 0000000..3adc6aa --- /dev/null +++ b/PLAN_PHOTO_FACE_AI.md @@ -0,0 +1,553 @@ +# План: Улучшение лиц на фотографиях (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 +``` + diff --git a/TODO_PHOTO_FACE_AI.md b/TODO_PHOTO_FACE_AI.md new file mode 100644 index 0000000..e316c3e --- /dev/null +++ b/TODO_PHOTO_FACE_AI.md @@ -0,0 +1,345 @@ +# TODO: фото-ИИ с восстановлением лиц (Real-ESRGAN + GFPGAN/CodeFormer), авто-выбор GPU/CPU + +Источник требований: `PLAN_PHOTO_FACE_AI.md`. Этот файл — **исполняемый чек-лист для агента**, +который пишет код. План не дублируется: здесь только задачи, якоря в коде, контракты и приёмка. + +Перед стартом агент обязан прочитать `AGENTS.md` (правила проекта) и `PLAN_PHOTO_FACE_AI.md` целиком. + +--- + +## 0. Жёсткие инварианты (нарушение = регресс, работа не принята) + +- [x] **I1. Обратная совместимость `/enhance`.** Запрос только с `image` + `scale=2` (без `model`/`face`) + обязан давать тот же JPEG, что и до изменений: `x2plus`, `half=False` на CPU, `IMWRITE_JPEG_QUALITY=92`. + Проверяется сравнением с эталоном из Stage 0. Эталон снят (см. «Журнал раздела 0»), прогон + `node backups/photo-face-ai-baseline/capture.js