План фото-ИИ с восстановлением лиц разбит на этапы; этот коммит закрывает
раздел 0 — семь инвариантов, которые нельзя ломать дальше. Два из них были
нарушены в текущем коде и исправлены здесь.
I5 (мягкие ошибки не сжигают попытки). Раньше любой сбой photo-ai —
503, обрыв сети, таймаут — попадал в общий catch, инкрементил attempts и
через три попытки переводил задание в error. Теперь ошибки разделены:
5xx/429/425/408 и сетевая недоступность возвращают задание в pending без
инкремента attempts, с экспоненциальной паузой 10 с → 300 с; лимит мягких
повторов (по умолчанию 60) даёт одну честную ошибку с понятным текстом.
Таймаут AbortSignal.timeout — жёсткая ошибка с попытками, как и раньше.
Счётчик мягких повторов живёт в памяти процесса и в счётчиках воркера,
метаданные повтора — в audit_log.target (soft_attempt/soft_limit), без
новых колонок. Новый аудит-код photo.job.soft_retry и подпись в audit.js.
Переменные PHOTO_AI_SOFT_MAX_RETRIES, PHOTO_AI_SOFT_BACKOFF_MS,
PHOTO_AI_SOFT_BACKOFF_MAX_MS описаны в .env.example и отдаются в
worker.config в GET /api/photo-jobs/status.
I7 (никаких прямых fs.* по uploads/). runAiEnhance писал результат
fs.writeFileSync в uploads/ и только потом persist в S3; enhanceWithSharp
делал то же через sharp toFile. Оба теперь считают буфер и пишут его
через storage.put — драйвер выбирает сам, локальной копии не остаётся.
Из worker.js убраны require('fs'), require('path') и параметр uploadsDir.
I3 (photo-ai не обязателен). photo_ai_enabled вычислялся внутри
cacheWrap('public-settings'), поэтому после перезапуска с пустым
PHOTO_AI_URL кнопка «🤖 ИИ» оставалась видимой до истечения кэша (60 с),
хотя enhance-ai уже отдавал 503. Флаг вынесен из кэша: он выводится из
PHOTO_AI_URL в памяти процесса и всегда актуален.
Проверено на стенде (журнал — в TODO_PHOTO_FACE_AI.md, раздел 0):
- I1: эталон /enhance снят на 5 фото (3 реальных, 2 синтетических),
два независимых прогона и прогон после правок совпали байт-в-байт
(sha256), /health отдаёт ok. Скрипты и эталон — в backups/ (вне git)
- I2: задание с params IS NULL и action='ai' дошло до done при
attempts=0, результат отдан из S3 (200)
- I3: с пустым PHOTO_AI_URL photo_ai_enabled=false сразу после старта,
enhance-ai → 503, остальные маршруты API живы
- I4: nvidia-ctk и nvidia-container-runtime на хосте отсутствуют, runtime
только runc — фото-ИИ поднялся на CPU, /health не падает. Проверка
PHOTO_AI_DEVICE переносится на приёмку Stage 1 (переменной ещё нет)
- I6: db/ не тронут, состав колонок photo_jobs прежний
- I7: node --check для всех изменённых JS, комментариев в диффе нет,
весь SQL параметризован
api.smoketest.js: контракт фото-воркера — согласованность
photo_ai_enabled и ai_configured, ключи мягких повторов в worker.config,
503/404 для enhance-ai на несуществующей записи (тест не создаёт реальных
заданий). Вместе с планом и чек-листом этапов.
40 KiB
План: Улучшение лиц на фотографиях (Real-ESRGAN + GFPGAN/CodeFormer) с автовыбором GPU/CPU
Статус: план, реализация не начата. Документ описывает целевую архитектуру, изменения по файлам, порядок внедрения и критерии приёмки. Реализацию начинать после знакомства с этим файлом.
1. Постановка задачи
Сейчас система улучшает фотографии одной моделью RealESRGAN_x2plus (x2, CPU, half=False,
device='cpu', захардкожено в photo-ai/app.py:44). Модель хорошо восстанавливает текстуры
(одежда, фон, бумага), но лица при апскейле часто получают артефакты, «пластиковую» кожу и
искажённые черты, потому что у RealESRGAN_x2plus нет приора на структуру лица.
Нужно:
- Добавить модели восстановления лиц — GFPGAN v1.4 и/или CodeFormer (оба — face restoration
с prior-сетью, работают в связке с
RealESRGANerкакbg_upsampler). - Дать пользователю выбор модели для обработки: универсальная x2, face-модель, комбинация
(фон + лица), быстрая VGG-модель
realesr-general-x4v3. - Поддержать работу на GPU и на CPU с автоматическим выбором устройства: есть рабочий CUDA — используем GPU, нет — молча и без падений уходим на CPU.
- Не допустить простоя GPU-контейнера: пока модели грузятся — сервис отвечает
503, воркер ждёт, а не теряет задания. - Не сломать существующие контракты:
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-модель нельзя ставить «всегда»
- Скорость. Детекция + face-restore + paste-back даёт +40…150 % ко времени. На CPU это разница между ~40 с и ~90 с на фото 640×480.
- Гарантий нет. GFPGAN «дорисовывает» лица по приору: на сильно замытых, боковых или закрытых лицах он может сделать человека похожим на другого. Для журнала посещаемости ошибка идентификации недопустима, поэтому face-режим — явный выбор пользователя, а не молчаливый дефолт.
- 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/<name>.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), модели грузятся лениво по требованию и кешируются вMODELSdict под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 остаётся):
{
"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 отсутствует):
# 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:
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-режима:
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
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
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) обрабатывает отдельно:sleep10 с,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
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.
PHOTO_JOB_ACTIONS— добавить'ai_face','ai_upscale'. Схема не меняется (action VARCHAR(20)), но белый список используется в двух местах:ensurePhotoJobsTable()(~1238–1256) и при разборе restore-данных (~2259) — обновить оба.GET /api/photo-jobs/status(строка 5782) — прокинутьservice.device,service.device_name,service.ready,service.vram_free_mb,service.models,service.face_modelsизGET /healthphoto-ai (таймаут 5 с, по образцуaiHealthCheck()). Запрос делать без падения: photo-ai недоступен →service = { reachable: false }.- Новый
GET /api/photo-ai/health(requireAdmin) — прямой прокси/healthдля оператора. ВgetStackInfo()добавить блокphoto_ai(device,device_name,vram_total_mb,models) — рендерится в «Статус стека» наpublic/settings.html. - Настройки — валидация рядом со строкой 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) — сохранить.
worker.jsвызывается с новыми параметрами: вcreatePhotoEnhanceWorker({...})(строка 6045) добавитьfaceTimeoutMs: PHOTO_AI_FACE_TIMEOUT_MS,defaultFaceModel: PHOTO_AI_FACE_MODEL.- Предупреждения сервиса (
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(строка 1settings.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. Новые переменные окружения
# === Фото-ИИ (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. Критерии готовности
photo-aiстартует и на CPU-хосте, и с CUDA, определяя устройство автоматически;/healthсообщает фактическое устройство, имя GPU, VRAM, доступные и загруженные модели.- Существующий сценарий «🤖 ИИ» без параметров даёт результат, эквивалентный текущему.
- Доступны режимы: универсальный x2, быстрый x4, только лица (GFPGAN), лица + фон.
- Задание с face-моделью проходит полный цикл
pending → processing → done; результат виден в сравнении «Было/Стало», применим вручную и откатывается. - Отсутствие GPU, отсутствие
nvidia-ctkи недоступныйphoto-aiне ломают приложение:PHOTO_AI_URL=пусто → кнопка «ИИ» недоступна; сервис упал → задания пережидают (503), не сжигая попытки; пустой.envполностью совместим. node api.smoketest.jsпроходит;AGENTS.md,README.md,.env.exampleописывают новые переменные и GPU-запуск.
10. Быстрые команды для проверки после реализации
# Статус устройства и моделей
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