План фото-ИИ с восстановлением лиц разбит на этапы; этот коммит закрывает
раздел 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 на несуществующей записи (тест не создаёт реальных
заданий). Вместе с планом и чек-листом этапов.
34 KiB
TODO: фото-ИИ с восстановлением лиц (Real-ESRGAN + GFPGAN/CodeFormer), авто-выбор GPU/CPU
Источник требований: PLAN_PHOTO_FACE_AI.md. Этот файл — исполняемый чек-лист для агента,
который пишет код. План не дублируется: здесь только задачи, якоря в коде, контракты и приёмка.
Перед стартом агент обязан прочитать AGENTS.md (правила проекта) и PLAN_PHOTO_FACE_AI.md целиком.
0. Жёсткие инварианты (нарушение = регресс, работа не принята)
- 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 <label> && node backups/photo-face-ai-baseline/verify.js <label>даёт байт-в-байт совпадение; повторная проверка обязательна на приёмке Stage 1. - I2. Старые задания.
photo_jobsсоparams IS NULLиaction='ai'продолжают обрабатываться воркером как раньше (дефолты подставляются на стороне воркера/сервиса). - I3. photo-ai не обязателен. Пустой
PHOTO_AI_URL→ кнопка «🤖 ИИ» скрыта (public/js/journal.js:718),POST /api/entries/:id/photo/enhance-ai→503(server.js:5115), приложение полностью работоспособно. - I4. GPU не обязателен. Нет
nvidia-ctk/ нет CUDA → контейнер поднимается,PHOTO_AI_DEVICE=cudaдаёт WARN в лог и молчаливый откат на CPU. Падение/healthиз-за отсутствия GPU недопустимо. Проверено на этом хосте:nvidia-ctkотсутствует, nvidia-runtime вdocker infoнет,photo-aiподнялся и/health→{"ok":true}(GPU недоступен → сервис не падает). ПеременнаяPHOTO_AI_DEVICEпоявится только в Stage 1 — проверкаcuda→ WARN + CPU откат переносится на приёмку Stage 1, сам GPU-режим на этом хосте не проверяется (стоп-условие: нуженnvidia-container-toolkit, ставить без разрешения оператора нельзя). - I5. Воркер не сжигает попытки на «мягких» ошибках.
503/ недоступный сервис → задание возвращается вpendingбез инкрементаattempts; лимит мягких повторов (например 60) → одна честная ошибка. - I6. Никаких новых колонок в БД. Метаданные обработки (модель, устройство, время,
faces_found) живут вphoto_jobs.params(JSONB уже есть) и вaudit_log.target. - I7. Код без комментариев (правило
AGENTS.md), CommonJS в Node-части, параметризованный SQL, никаких прямыхfs.*поuploads/(толькоstorage.*).
Журнал раздела 0 (проверено 2026-09-28)
Что сделано, кроме проверки: в worker.js устранены два нарушения инвариантов — прямой
fs.writeFileSync в uploads/ вместо storage.put (I7) и сжигание attempts на 503/недоступности
(I5). В server.js флаг photo_ai_enabled вынесен из кэша public-settings (I3). Новые переменные
PHOTO_AI_SOFT_* описаны в .env.example.
| Инвариант | Как проверено | Результат |
|---|---|---|
| I1 | capture.js → before/, повтор repeat1/, прогон после правок after-section0/; verify.js сравнивает sha256 |
5/5 байт-в-байт, /health {"ok":true} |
| I2 | запись INSERT INTO photo_jobs (entry_id, action, status, params) VALUES (385,'ai','pending',NULL) |
done, attempts=0, результат отдаётся из S3 (200) |
| I3 | docker compose -f docker-compose.yml -f no-photo-ai.yml up -d app (PHOTO_AI_URL: "") |
photo_ai_enabled=false, enhance-ai → 503, 8 маршрутов API живы |
| I4 | backups/photo-face-ai-baseline/env.txt |
nvidia-ctk NOT_FOUND, nvidia-container-runtime NOT_FOUND, runtimes runc/io.containerd.runc.v2, photo-ai Up, /health ok |
| I5 | photo-ai остановлен → задание ждало 70 с → вернулось pending при attempts=0; после docker compose start photo-ai → done при attempts=0. Лимит проверен прогоном с PHOTO_AI_SOFT_MAX_RETRIES=2 |
мягкий повтор не тратит попытки; после лимита status=error, attempts=0, текст «мягкие повторы исчерпаны (2)» |
| I6 | git status db/ пуст, information_schema.columns для photo_jobs — те же 12 колонок; метаданные повторов в audit_log.target (soft_attempt, soft_limit) |
без изменений схемы |
| I7 | node --check worker.js server.js api.smoketest.js public/js/audit.js; grep по worker.js — ни require('fs'), ни require('path'), ни uploadsDir; в диффе нет строк с комментариями; все SQL — на $1/$2 |
чисто |
Эталон и скрипты — в backups/photo-face-ai-baseline/ (вне git): env.txt, inputs/, inputs.json,
before/, repeat1/, after-section0/, capture.js, verify.js, make-inputs.js.
Тестовые задания (id 90–94), их аудит, уведомления и 3 объекта результата в S3 удалены.
1. Что уже есть (сверено в коде, не перепроверять)
| Место | Сейчас | Что меняем |
|---|---|---|
photo-ai/app.py:15–73 |
одна модель, MODEL_PATH, MAX_INPUT_PIXELS, lock+upsampler, /health → {ok}, /enhance(image, scale) |
реестр моделей, pick_device(), ModelPool, расширенные /health и /models |
photo-ai/Dockerfile |
python:3.10-slim, torch --index-url .../whl/cpu, realesrgan==0.3.0, патч basicsr/data/degradations.py |
ARG TORCH_VARIANT, gfpgan/facexlib, вендоринг CodeFormer |
docker-compose.yml:194–204 |
photo-ai: MODEL_PATH, MAX_INPUT_PIXELS, том photo-ai-models, без ports/expose |
новые env + healthcheck |
docker-compose.yml:208 |
том photo-ai-models |
без изменений |
server.js:1841 |
PHOTO_AI_URL |
+ константы PHOTO_AI_FACE_MODEL, PHOTO_AI_FACE_TIMEOUT_MS |
server.js:1237–1256 |
ensurePhotoJobsTable(): колонка action VARCHAR(20), params JSONB, CHECK на action нет |
миграция схемы не требуется |
server.js:2254 |
PHOTO_JOB_ACTIONS = new Set(['ai','enhance','restore','rollback']) |
+ 'ai_face', 'ai_upscale' |
server.js:5114–5136 |
POST .../enhance-ai: без тела → action='ai', params=NULL |
приём/валидация {model, face, face_model, strength} |
server.js:5782–5837 |
GET /api/photo-jobs/status: service = await aiHealthCheck() вычисляется, но в ответ не попадает |
вернуть service (это баг-дыра, см. D2) |
server.js:769–826 |
getStackInfo(): app/deps/runtime/database/cache/storage |
+ блок photo_ai |
server.js:1663–1664 |
keys/defaults для /api/public-settings |
+ photo_ai_face_mode, photo_ai_face_model, photo_ai_device_pref |
server.js:1710 |
валидация photo_enhance_engine в PUT /api/settings |
+ валидация трёх новых ключей |
server.js:6045–6056 |
createPhotoEnhanceWorker({...}) |
+ faceTimeoutMs, defaultFaceModel |
worker.js:11 |
PHOTO_AI_TIMEOUT_MS = 300000 (хардкод) |
читать env, добавить face-таймаут |
worker.js:35–41 |
CONFIG воркера |
+ face_model, default_model, face_timeout_ms |
worker.js:122–141 |
runAiEnhance(srcKey): image + scale=2, ждёт сырой image/jpeg |
проброс params, Accept: application/json, разбор JSON, 503 без траты попыток |
worker.js:176 |
job.action === 'ai' ? runAiEnhance : enhanceWithSharp |
+ 'ai_face', 'ai_upscale' |
worker.js:186–192 |
инкремент attempts и photo.job.retry |
не инкрементить при 503/недоступности |
public/js/journal.js:606–615 |
loadEnhanceEngine() читает /api/public-settings |
+ чтение дефолта face-режима |
public/js/journal.js:617–671 |
runPhotoAi(): confirm() + POST без тела |
+ селект модели, тело {model, face, face_model} |
public/js/journal.js:718 |
показ кнопки по photo_ai_enabled |
без изменений (I3) |
public/js/worker.js:30–35 |
PHOTO_ACTION_LABELS |
+ ai_face, ai_upscale |
public/js/worker.js:300–308 |
openPhotoJob(r) — модалка «Было/Стало» |
+ model/device/faces_found/elapsed_ms |
public/js/settings.js:1 |
DIRTY_FIELDS |
+ новые поля |
public/js/settings.js:217–218, 739 |
загрузка/сохранение photo_enhance_engine |
+ face-режим, face-модель, желаемое устройство |
public/js/settings.js:372–477 |
renderStackInfo() — 5 карточек |
+ карточка «Фото-ИИ» |
public/settings.html:221–260 |
карточка #sec-photo (камера) |
+ блок «Фото-ИИ» (или новая карточка) |
public/js/audit.js:34–48 |
словарь photo.* |
+ новые коды аудита |
db/init.sql:242, db/migration.sql:234 |
INSERT ... 'photo_worker_enabled' |
+ дефолты новых настроек |
.env.example:39–41 |
PHOTO_AI_URL, PHOTO_AI_MAX_PIXELS |
+ остальные переменные |
photo-ai в compose |
порт не проброшен; text-corrector занимает 8080:8080 |
см. D1 — ручные curl из плана не сработают |
2. Расхождения плана с кодом — решить ДО правок
- D1. Порт для ручных проверок.
photo-aiне имеетports/expose, а хостовый8080занятtext-corrector. Команды видаcurl http://localhost:8080/enhanceиз §10 плана попадут вtext-corrector. Решение: проброситьports: ["127.0.0.1:8081:8080"]в сервисphoto-ai(только loopback — правило «не публиковать наружу» соблюдено) или выполнять ручные проверки черезdocker compose exec -T app node -e .... Выбрать одно и применить; вREADMEи.env.exampleотразить выбранный способ. - D2.
serviceв статусе фото-воркера.server.js:5825вызываетaiHealthCheck()(это health текстового ИИ), результат не возвращается. Нужен отдельныйphotoAiHealth()с таймаутом 5 с, обращающийся к${PHOTO_AI_URL}/health, и его результат в ответе/api/photo-jobs/status. При недоступности —{ reachable: false }, без исключения. - D3. Где живёт белый список действий. План говорит про два места (
ensurePhotoJobsTable()и restore). По фактуPHOTO_JOB_ACTIONSиспользуется только в нормализации restore-данных (server.js:2254), а вensurePhotoJobsTable()CHECK-ограничения наactionнет. Обновить одно место; при желании вынести набор на уровень модуля, чтобы его нельзя было забыть. - D4. Доступность пакетов. Проверить
pip index/зеркало наличиеgfpganиfacexlib(pip download gfpgan==1.3.8 facexlib==0.3.0 -d /tmp/x --no-deps). Если недоступны — вендорить вphoto-ai/vendor/и копировать в образ. CodeFormer официальным pip-пакетом не распространяется. - D5. CodeFormer — опционален по умолчанию. Реализовать как запись реестра, которая включается,
только если вендоренный модуль импортируется. Если нет:
face_modelsв/healthего не содержит, API отвечает400с понятным текстом, UI не показывает его в списке. Сборка и CPU-режим не падают. - D6. Поведение без
photo-aiв compose. СейчасPHOTO_AI_URLпо умолчаниюhttp://photo-ai:8080(docker-compose.yml:79) — то есть «ИИ» включён по умолчанию. Не менять дефолт молча; если меняется — явно записать в.env.exampleиREADME.
3. Stage 0. Подготовка и эталон «до» (обязательно до любых правок кода)
- Зафиксировать окружение:
docker --version,docker compose version,nvidia-smi, наличие/отсутствиеnvidia-ctk,docker info | grep -i runtime. - Если
nvidia-ctkнет — зафиксировать это как «GPU-режим не проверяем на этом хосте», не пытаться ставить системные пакеты без явного разрешения оператора. - Сохранить текущее состояние
.env(PHOTO_AI_URLпусто или задан). - Поднять текущий стек как есть:
docker compose up -d --build. - Снять эталон «до» на 3–5 фото (портрет, групповое, без лиц, зашумлённое 640×480):
результат
/enhanceсscale=2без других параметров +/health. Сохранить файлы и размеры вbackups/photo-face-ai-baseline/(вне git). - Приёмка: эталон сохранён,
curl /healthотвечает, текущий сценарий «🤖 ИИ» даётdone.
4. Stage 1. photo-ai/app.py: реестр моделей + авто-выбор устройства
pick_device() -> (device, half, device_name)по §4.1 плана: env → cuda → mps → cpu;half=Trueтолько на CUDA; явныйcudaбез CUDA = WARN + cpu; явныйcpu= всегда cpu.MODEL_REGISTRY:x2plus(RRDBNet(scale=2)),general-x4v3(SRVGGNetCompact(upscale=4)+wdn),animevideo-v3(SRVGGNetCompact(upscale=4)).FACE_REGISTRY:gfpgan(GFPGANer(arch='clean', channel_multiplier=2, upscale=2, bg_upsampler=…)),codeformer(заD5).- Загрузка весов: список URL из §3.4 плана, каталог
${PHOTO_AI_MODELS_DIR:-/models}/weights/, скачивание в.tmp→os.replace, проверка минимального размера, кэш в томе.MODEL_PATHчитается как алиас дляx2plus(существующий.env/том не ломается). class ModelPool:threading.Lock, ленивая загрузка по требованию, кеш, LRU с лимитом 2,PHOTO_AI_LOAD_ALL=1— предзагрузка, прогрев на синтетическом шуме 64×64 после загрузки.- OOM-деградация:
RuntimeErrorс CUDA OOM →tileпополам (256→128→64), один ретрай; повтор → инвалидация модели, переход на CPU, ещё одна попытка; финал —500с понятным текстом. GET /health— контракт §4.3 плана (полеokсохраняется, добавляютсяready,device,device_name,half,tile,driver,cuda,vram_total_mb,vram_free_mb,models,face_models,loaded,loading,max_pixels).GET /models— список моделей, face-моделей, устройство, дефолты.POST /enhance: поляimage,scale,model(дефолтx2plus),face(off|face|all, дефолтoff),face_model(gfpgan|codeformer),strength(0..1, только CodeFormer, дефолт 0.7),jpeg_quality(70..100, дефолт 92). Неизвестная модель →400со списком допустимых. Модель не готова →503+Retry-After: 5.- Два формата ответа: сырой
image/jpegпо умолчанию (совместимость) и JSON приAccept: application/json:{ok, image_base64, model, face, face_model, faces_found, device, elapsed_ms, warnings}. Приface != off—enhance(..., has_aligned=False, only_center_face=False, paste_back=True); лица не найдены — не ошибка,faces_found: 0+ чистыйbg_upsampler. - Расширение выходного файла — по имени файла, не по
content_type(воркер шлётimage/jpegдля всего). - Проверка I1: эталон из Stage 0 воспроизводится (сравнить размер/содержимое,
node --check-эквивалент для Python —python -c "import ast;ast.parse(open('photo-ai/app.py').read())"). - Приёмка:
/healthотдаётdevice/device_name/half;PHOTO_AI_DEVICE=cpuпри рабочей CUDA →cpu;PHOTO_AI_DEVICE=cudaбез CUDA →cpu+ WARN, сервис поднялся;PHOTO_AI_DEVICE=autoбез CUDA →cpu.
5. Stage 2. photo-ai/Dockerfile + compose
ARG TORCH_VARIANT=cpuиARG TORCH_INDEX=https://download.pytorch.org/whl/${TORCH_VARIANT}; один образ,cu124— вариант сборки.- Сохранить патч
basicsr/data/degradations.py(functional_tensor→functional) — без него basicsr падает на torch ≥ 2.0. Не «упрощать» Dockerfile без проверки. - Сохранить
libgl1 libglib2.0-0(нужны facexlib),numpy<2,opencv-python-headless. - Добавить
gfpgan/facexlib(или вендоринг поD4), вендоренный CodeFormer копировать в образ при наличии (D5). ENV PHOTO_AI_MODELS_DIR=/models;MODEL_PATHостаётся валидным алиасом.docker-compose.yml, сервисphoto-ai: envPHOTO_AI_DEVICE,PHOTO_AI_FACE_MODEL,PHOTO_AI_TILE,PHOTO_AI_MAX_PIXELS,PHOTO_AI_LOAD_ALL,PHOTO_AI_JPEG_QUALITY;healthcheckсstart_period: 300s; решение по порту изD1.- Новый
docker-compose.gpu.yml(по образцуdocker-compose.minio.yml):build.args.TORCH_VARIANT=cu124,PHOTO_AI_DEVICE=cuda,deploy.resources.reservations.devices(driver: nvidia,count: 1). Без него стек поднимается на любой машине. - Сервису
appдобавить envPHOTO_AI_FACE_MODELиPHOTO_AI_FACE_TIMEOUT_MS. - Приёмка: CPU-сборка стартует;
/api/photo-jobs/statusпоказываетservice.device; при пустомPHOTO_AI_URLприложение работает полностью (I3);docker compose -f docker-compose.yml -f docker-compose.gpu.yml configвалиден (сам GPU-пуск — только если toolkit есть).
6. Stage 3. Face-режим (GFPGAN) в app.py
face=off— только ESRGAN (текущее поведение).face=face— ESRGAN-фон + GFPGANpaste_back.face=all— ESRGAN(+wdnдляgeneral-x4v3) + мягкий денойз фона + лица.strength→fidelity_weightCodeFormer,-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 без падения сервиса.
7. Stage 4. worker.js + server.js
7.1 worker.js (createPhotoEnhanceWorker)
- Таймауты из env:
PHOTO_AI_TIMEOUT_MS(дефолт 300000),PHOTO_AI_FACE_TIMEOUT_MS(дефолт 600000) дляface != 'off'. Таймаут выбирается на основеparams, а не глобально. runAiEnhance(srcKey, params): отправляетimage,scale,model,face,face_model,strengthизparams(дефолты при пустыхparams); ставитAccept: application/json; принимает и JSON (base64 → буфер), и сыройimage/jpeg(старый photo-ai) без ошибки.503/Retry-After/ сетевая недоступность →sleepc экспоненциальной задержкой,return false,attemptsне инкрементится; лимит мягких повторов (например 60) → одна честная ошибка с понятным текстом (I5).AbortErrorотAbortSignal.timeout→ сообщение «ИИ-сервис не ответил за N с» — это уже «жёсткая» ошибка с попытками.processOne:action∈ {ai,ai_face,ai_upscale} → AI-путь;enhance→ sharp. Пустыеparamsприai_face→face='face'.applyResult: вlogAudit(..., 'photo.job.preview', {...})добавитьmodel,face,face_model,device,elapsed_ms,faces_found,warnings. Текст уведомленияphoto.job.done— по факту режима (job.action === 'ai_face'→ «Фото обработано нейросетью с восстановлением лиц»).CONFIGворкера: +face_model,default_model,face_timeout_ms(попадают вGET /api/photo-jobs/status→worker.config).
7.2 server.js
POST /api/entries/:id/photo/enhance-ai(server.js:5114): принять{model, face, face_model, strength}; валидация инлайн-хелперами и явными списками (какPUT /api/settings), невалидное →400.params = JSON.stringify({model, face, face_model, strength});action = face === 'off' ? 'ai' : 'ai_face'. Пустое тело → сегодняшнее поведение (params = NULL,action='ai') — I2.PHOTO_JOB_ACTIONS(server.js:2254): +'ai_face','ai_upscale'(см.D3). Схемаaction VARCHAR(20)вмещает новые значения — миграция не нужна.photoAiHealth()(см.D2) + проксирование вGET /api/photo-jobs/status→service(reachable,device,device_name,ready,vram_total_mb,vram_free_mb,models,face_models,loaded). Недоступен →{reachable:false}, ответ 200.GET /api/photo-ai/health(requireAdmin) — прямой прокси/healthphoto-ai для оператора.getStackInfo()(server.js:769) — блокphoto_ai(engine: 'Real-ESRGAN + GFPGAN',driver,device,device_name,vram_total_mb,models). Без credentials, только hostname.- Настройки (
PUT /api/settings, рядомserver.js:1710):photo_ai_face_mode∈off|face|all(дефолтoff),photo_ai_face_model∈gfpgan|codeformer(дефолтgfpgan),photo_ai_device_pref∈auto|cuda|cpu(дефолтauto, только для UI/доков — фактическое устройство задаёт контейнер). - Дефолты новых настроек:
db/init.sql,db/migration.sql(INSERT ... ON CONFLICT DO NOTHING),keys/defaultsв/api/public-settings(server.js:1663–1664).photo_ai_enabled(строка 1677) сохранить. createPhotoEnhanceWorker({...})(server.js:6045): +faceTimeoutMs: PHOTO_AI_FACE_TIMEOUT_MS,defaultFaceModel: PHOTO_AI_FACE_MODEL.warningsот photo-ai сохранять в аудит/уведомление, чтобы оператор видел, что face-режим не сработал не из-за ошибки.- Приёмка: задание с
face='face'проходитpending → processing → done;paramsсохранены; аудит содержит модель/устройство/время; остановка photo-ai на лету → задание дорабатывается после возврата сервиса безerror(I5); результат применяется и откатывается как раньше.
8. Stage 5. Фронтенд
public/journal.html+public/js/journal.js: рядом с «🤖 ИИ» селект режима («Универсально (x2)», «Быстро (x4)», «Лица (GFPGAN)», «Лица + фон»); значение уходит вPOST .../enhance-aiтелом{model, face, face_model}. Дефолт — изphoto_ai_face_mode(/api/public-settings,loadEnhanceEngine); приdevice === 'cpu'— подсказка «на CPU медленно». Кнопка по-прежнему скрыта приphoto_ai_enabled === 'false'(I3).public/js/worker.js:PHOTO_ACTION_LABELS+ai_face: 'ИИ + лица',ai_upscale: 'ИИ-апскейл'; в модалке сравнения —model,device,faces_found,elapsed_ms(данные изparams/аудита, I6); строка состояния учитываетservice.reachable === falseиservice.device.public/settings.html+public/js/settings.js: блок «Фото-ИИ» — селект face-режима, селект face-модели, селект желаемого устройства + строка «фактическое: …» с подсветкой расхождения; read-only статус (device_name, VRAM, список моделей) из/api/photo-jobs/status. Новые id внести вDIRTY_FIELDS(settings.js:1) и в payload (settings.js:739).public/js/settings.js:372renderStackInfo()— карточка «Фото-ИИ» изstack.photo_ai.public/js/audit.js— метки для новых кодов аудита (иначе в UI будет сырой код).- Приёмка: из журнала доступны все 4 режима; после постановки видно «В очереди», затем сравнение «Было/Стало» с моделью/устройством; в настройках видно фактическое устройство и VRAM.
9. Stage 6. Документация и тесты
AGENTS.md: раздел про photo-ai (реестр моделей, device-политика, новые env, GPU-override, контракт/enhanceи/health), строки в таблице «File Map» дляphoto-ai/app.py,photo-ai/Dockerfile,docker-compose.gpu.yml.README.md: установка NVIDIA Container Toolkit, запуск сdocker-compose.gpu.yml, проверкаGET /api/photo-jobs/status→service.device, ручной вызов/enhance(с учётомD1)..env.example: все переменные из §7 плана с комментариями.api.smoketest.js(по правилуAGENTS.md— контракт API фиксируется в том же изменении): -POST /api/entries/:id/photo/enhance-aiсmodel: 'нет такой'→400; -face: 'face'безPHOTO_AI_URL→503(условно: еслиai_configured === false); -GET /api/photo-jobs/statusсодержит ключservice.- Приёмка:
node api.smoketest.jsпроходит;node --check server.js,node --check worker.js,python -c "import ast;ast.parse(...)"дляapp.py;docker compose configвалиден для обоих compose-файлов; документация совпадает с кодом.
10. Новые переменные окружения (итог)
| Переменная | Умолчание | Где используется |
|---|---|---|
PHOTO_AI_URL |
http://photo-ai:8080 (compose), пусто = выкл. |
server.js, воркер |
PHOTO_AI_MAX_PIXELS |
4000000 |
app.py (алиас MAX_INPUT_PIXELS) |
PHOTO_AI_DEVICE |
auto |
app.py (`auto |
PHOTO_AI_TILE |
256 |
app.py |
PHOTO_AI_FACE_MODEL |
gfpgan |
app.py, дефолт воркера/UI |
PHOTO_AI_LOAD_ALL |
0 |
app.py |
PHOTO_AI_FACE_MODE |
off |
дефолт UI |
PHOTO_AI_JPEG_QUALITY |
92 |
app.py |
PHOTO_AI_TIMEOUT_MS |
300000 |
worker.js (сейчас хардкод) |
PHOTO_AI_FACE_TIMEOUT_MS |
600000 |
worker.js |
PHOTO_AI_SOFT_MAX_RETRIES |
60 |
worker.js — лимит мягких повторов (I5) |
PHOTO_AI_SOFT_BACKOFF_MS |
10000 |
worker.js — первая пауза мягкого повтора |
PHOTO_AI_SOFT_BACKOFF_MAX_MS |
300000 |
worker.js — потолок паузы |
PHOTO_AI_MODELS_DIR |
/models |
app.py (внутри контейнера) |
11. Порядок и правила выполнения
- Этапы выполняются строго 0 → 1 → 2 → 3 → 4 → 5 → 6. Каждый этап закрывается своей приёмкой до начала следующего.
- Каждый завершённый чекбокс отмечать (
- [x]) в этом файле по мере выполнения. - Изменения в
AGENTS.md/.env.example/README.mdделать в том же изменении, что и код, который они описывают (правилоAGENTS.mdпро рассинхрон документации). - Коммит на этап, а не на весь план:
app.py+Dockerfile+compose → воркер/сервер → фронт → доки/тесты. - Стоп-условия (сообщить оператору, не «чинить» самостоятельно): требуется
sudoна хосте; требуется установкаnvidia-container-toolkit; нашёлся конфликт миграции БД; текущий/enhanceперестал давать эталонный результат (I1).
12. Готово, когда
photo-aiстартует на CPU и на CUDA, устройство выбирается автоматически,/healthпоказывает фактическое устройство, имя GPU, VRAM, доступные и загруженные модели.- Сценарий «🤖 ИИ» без параметров даёт результат, эквивалентный текущему.
- Доступны режимы: универсальный x2, быстрый x4, только лица (GFPGAN), лица + фон.
- Face-задание проходит полный цикл, результат виден в сравнении, применяется и откатывается.
- Отсутствие GPU, отсутствие
nvidia-ctkи недоступныйphoto-aiне ломают приложение. node api.smoketest.jsпроходит;AGENTS.md,README.md,.env.exampleописывают новые переменные и GPU-запуск.