Commit Graph
7 Commits
Author SHA1 Message Date
dev 45033bffa8 feat(photo-ai): Stage 5 — фронтенд фото-ИИ (режимы, статус, метаданные)
Только фронтенд: server.js, worker.js, photo-ai/, compose и схема БД не тронуты.
Контракт /enhance-ai, /api/photo-ai/health и service в /api/photo-jobs/status
взят из Stage 4 как есть (I6 — новых колонок нет, метаданные из photo_jobs.params
и audit_log.target).

journal.html + journal.js:
- селект режима рядом с «🤖 ИИ»: Универсально (x2) / Быстро (x4) / Лица /
  Лица + фон; PHOTO_AI_MODES — единственная таблица режим→{model, face}
- значение уходит в POST .../enhance-ai телом {model, face, face_model};
  face_model подставляется только когда face !== 'off'
- дефолт селекта из photo_ai_face_mode (face=all → facesbg, face=face → faces)
- подсказка про медленный CPU: loadPhotoAiDevice() читает
  GET /api/photo-ai/health (только для админа)
- текст подтверждения и надпись «ИИ обрабатывает…» различают апскейл и face-режим
- кнопка и селект скрываются вместе при photo_ai_enabled === 'false' (I3)

settings.html + settings.js:
- новый раздел «Фото-ИИ»: режим по умолчанию, модель лиц, желаемое устройство
- read-only статус из /api/photo-jobs/status: фактическое устройство,
  device_name, VRAM, модели/модели лиц/загруженные; жёлтым — расхождение с
  желаемым устройством, красным — недоступность и незаданный PHOTO_AI_URL
- renderStackInfo() рисует карточку «Фото-ИИ» из stack.photo_ai
- новые id в DIRTY_FIELDS и в payload PUT /api/settings
- photo_ai_device_pref документирующий: фактическое устройство задаёт
  PHOTO_AI_DEVICE в контейнере

worker.html + worker.js:
- PHOTO_ACTION_LABELS: ai_face «ИИ + лица», ai_upscale «ИИ-апскейл»
- в модалке сравнения — чипы с моделью, режимом лиц, найденными лицами,
  устройством, временем обработки и предупреждениями
- строка состояния учитывает service.reachable === false («задания ждут»)
  и дописывает «· расчёт на <device>»

audit.js: 10 меток для кодов, которые раньше показывались сырыми (профиль и
фото ученика, главное фото, фото модуля, удаление/очистка уведомлений, логотип).

Проверено: node --check для всех четырёх js. Приёмка на живом стеке в этом
изменении не гонялась — чекбокс приёмки Stage 5 в TODO_PHOTO_FACE_AI.md
оставлен пустым, там же журнал раздела 5 с замечаниями для Stage 6.
2026-09-30 11:15:54 +03:00
dev 884188e9ec feat(photo-ai): Stage 4 — face-режим, апскейл-модели и метаданные в аудите
Воркер и сервер принимают параметры ИИ-обработки фото: модель апскейла
(x2plus / general-x4v3 / animevideo-v3), режим лиц (off / face / all),
модель лиц (gfpgan / codeformer) и strength. Пустое тело запроса ведёт себя
как раньше: action='ai', params=NULL (инвариант I2).

worker.js:
- таймаут выбирается по params.face: PHOTO_AI_TIMEOUT_MS для апскейла,
  PHOTO_AI_FACE_TIMEOUT_MS (600000) для face-режима
- runAiEnhance шлёт model/face/face_model/strength и понимает оба
  контракта: JSON с image_base64 и сырой image/jpeg старого сервиса
- тело не-2xx ответа больше не выбрасывается: readErrorBody() добавляет
  причину к сообщению, иначе оператор видит «ИИ-сервис ответил 400» без
  объяснения
- applyResult пишет в аудит model/face/face_model/device/faces_found/
  elapsed_ms/warnings и выбирает текст уведомления по факту режима;
  warnings видны оператору, если лица не нашлись
- CONFIG: + face_timeout_ms, default_model, face_model

server.js:
- POST /api/entries/:id/photo/enhance-ai принимает и валидирует тело до
  запроса записи — невалидный вход даёт 400, а не 404/500
- PHOTO_JOB_ACTIONS вынесен на уровень модуля, + ai_face и ai_upscale
- GET /api/photo-ai/health (requireAdmin) — прямой прокси /health
- photoAiHealth(timeoutMs), в «Статусе стека» вызывается с 2000 мс
- getStackInfo(): блок photo_ai (engine, host, device, vram, модели)
- настройки photo_ai_face_mode / photo_ai_face_model / photo_ai_device_pref
  с валидацией в PUT /api/settings, дефолты в init.sql, migration.sql,
  public-settings и ensurePhotoJobsTable()

Приёмка (живой стек, CPU + отдельно CUDA) — в TODO_PHOTO_FACE_AI.md,
журнал раздела 4: I1 байт-в-байт 5/5 и совпадение sha256 с raw-путём,
I2, I3 при пустом PHOTO_AI_URL, I5 на обрыве и на 503 с Retry-After,
7 невалидных тел → 400, api.smoketest.js 57 PASS.
2026-09-29 15:08:46 +03:00
dev 4c63a46d24 fix(photo-ai): лестница OOM на CUDA + приёмка face-режима и GPU-оверрайда
Stage 3 закрыт. Face-режим (off/face/all, strength, warnings) был написан в Stage 1;
здесь доведена приёмка и найден баг, который невозможно было увидеть без GPU-прогона.

Главное: realesные OOM никогда не доходили до run_guarded. И realessrgan/utils.py, и
gfpgan/utils.py ловят RuntimeError вокруг вызова сети и идут дальше
(`except RuntimeError as error: print('Error', error)`), поэтому на нехватку памяти
realesrgan падал уже не RuntimeError, а UnboundLocalError на присваивании
output_tile. Наружу уходил голый 500 «Internal Server Error»: ни лестницы тайлов,
ни деградации на CPU, ни внятного текста. В gfpgan это было тихое ухудшение —
при OOM лицо молча оставалось исходным, а задание уходило в «успех».

Починка: guard_forward() оборачивает forward сетей, которые строим мы
(RealESRGANer.model, restorer.gfpgan, CodeFormer net) и превращает OOM-RuntimeError
в TileOOM. TileOOM не наследует RuntimeError, поэтому проглатывающие except его
пропускают; is_oom и обе точки run_guarded ловят его явно. Обёртка вешается на
экземпляр, идемпотентна по флагу _photo_ai_guarded и не трогает класс.

Также добавлены два предупреждения из чек-листа, которых в коде не было: вход меньше
320×320 (лица могут не найтись) и CodeFormer на не-CUDA. Предупреждение «лица не
найдены» и деградация на CPU были на месте и не менялись.

Проверено на RTX 3050 Laptop (4096 МБ, драйвер 615.71.09, CUDA 12.6):
- tile=2048, x2plus face=all, полное фото: до правки 500 + UnboundLocalError,
  после 200 за 28.3 с с единственным warning «не хватило памяти при tile=2048»;
- инъекция OOM: лестница 2048 → 1024 → 512, затем переход на CPU (device: cpu,
  half: false) и успешный повтор; при повторе уже на CPU — честная 500 с подсказкой
  про PHOTO_AI_TILE / PHOTO_AI_MAX_PIXELS;
- 640×480: off 1.0 с / face 3.8 с / all 2.2 с, faces_found=6; фото без лиц даёт
  faces_found=0 и байты, равные face=off;
- I1: 6/6 MATCH байт-в-байт против Stage 2 на CPU (jpg/.jpeg/png+70/webp+100/x4v3/anime).

docker-compose.gpu.yml: GPU выдаётся через CDI (device_ids nvidia.com/gpu=all) —
не требует правки /etc/docker/daemon.json и перезапуска демона, в отличие от
классического резервирования driver: nvidia. TORCH_VARIANT cu124 → cu126: в индексе
cu124 последний torch 2.6.0, а cu126 даёт те же 2.14.0/0.29.0, что и CPU-образ, так
что варианты сборки отличаются только CUDA-библиотеками. Образ тегируется отдельно
(whatido-photo-ai:cu126), чтобы сборка GPU-варианта не перетирала CPU-образ
whatido-photo-ai:latest — откат остаётся обычным docker compose up -d photo-ai.

README: раздел «Запуск на NVIDIA GPU» с установкой NVIDIA Container Toolkit и генерацией
CDI-спеки, оговорками про 4 ГБ VRAM (x2plus + gfpgan влезают, general-x4v3 тяжелее,
LOAD_ALL=1 лучше не включать) и описанием параметров /enhance. .env.example: команда
GPU-запуска и рекомендация по PHOTO_AI_TILE. Журналы раздела 3 и GPU-прогона — в
TODO_PHOTO_FACE_AI.md.

worker.js, server.js, схема БД и фронтенд не тронуты — они в Stage 4…6.
2026-09-29 12:02:03 +03:00
dev 7367d66ac3 build(photo-ai): CPU/GPU сборка, gfpgan+facexlib без dev-зависимостей, healthcheck
Stage 2: photo-ai/Dockerfile получил ARG TORCH_VARIANT/TORCH_INDEX (один образ,
cu124 — вариант сборки) и явные пины gfpgan==1.3.8 / facexlib==0.3.0 через
--no-deps: tb-nightly и yapf (dev-зависимости gfpgan/basicsr) больше не попадают
в рантайм, matplotlib остаётся как зависимость filterpy (требование facexlib).
Патч basicsr/data/degradations.py сохранён байт-в-байт — без него basicsr падает
на torch >= 2.0.

Пин numpy<2 был невыполним: opencv-python-headless 5.0.0.93 требует numpy >= 2.
Зафиксирована фактическая версия numpy==2.2.6 в общем вызове pip install, а
opencv-python (не-headless) больше не ставится — раньше он приходил через gfpgan
и перезаписывал headless-сборку (активным был cv2 с GUI: QT5). I1 после этого
перепроверен: 5/5 MATCH байт-в-байт против эталона Stage 0.

ENV PHOTO_AI_MODELS_DIR=/models, MODEL_PATH остаётся валидным алиасом.
COPY vendor/ ./vendor/ + vendor/.gitkeep — вендоренный CodeFormer (D5)
подхватится из sys.path без правок Dockerfile.

docker-compose.yml: у photo-ai новые env (DEVICE, TILE, FACE_MODEL, LOAD_ALL,
JPEG_QUALITY, MODELS_DIR) и healthcheck со start_period 300s; MAX_INPUT_PIXELS
заменён на канонический PHOTO_AI_MAX_PIXELS (старое имя читается app.py как алиас).
У app — PHOTO_AI_FACE_MODEL и PHOTO_AI_FACE_TIMEOUT_MS (воркер читает их в Stage 4).
Новый docker-compose.gpu.yml (по образцу minio): TORCH_VARIANT=cu124,
PHOTO_AI_DEVICE=cuda, deploy.resources.reservations.devices для nvidia.

.env.example и таблица переменных README описывают ровно те переменные, которые
теперь подставляет compose; блок про GPU и NVIDIA Container Toolkit — Stage 6 (D6).

Проверено на живом стеке: сборка EXIT=0 (2.63 ГБ), /health отдаёт device=cpu,
face_models=[gfpgan], контейнер healthy, /api/photo-jobs/status отдаёт
service.device, I3 (пустой PHOTO_AI_URL -> 503 + 8 маршрутов 200), I4
(PHOTO_AI_DEVICE=cuda без CUDA -> WARN + CPU), docker compose config валиден для
базы, minio и gpu. GPU-пуск не выполнялся: nvidia-ctk на хосте отсутствует.
2026-09-29 09:35:40 +03:00
dev e8c15cc425 feat(photo-ai): реестр моделей и авто-выбор устройства
Stage 1: переписан photo-ai/app.py. Реестр моделей вместо одной модели,
авто-выбор cuda/mps/cpu, ModelPool с ленивой загрузкой, прогревом и LRU
на 2 записи, OOM-деградация по лестнице тайлов, расширенные /health
и /models, /enhance с выбором модели, режима лиц и качества JPEG.

Инвариант I1 держится: запрос только с image + scale=2 по-прежнему даёт
байт-в-байт тот же JPEG (5/5 MATCH против эталона Stage 0). Расширение
выводится по имени файла, а не по content_type, и .jpeg нормализуется
в .jpg — этого хватает для совместимости с воркером, который шлёт
image/jpeg для всего.

/enhance отдаёт сырой JPEG по умолчанию и JSON при Accept: application/json.
Пока модель грузится — 503 с Retry-After: 5; worker.js относит status >= 500
к мягким, поэтому попытка не тратится (I5).

Новые переменные (PHOTO_AI_DEVICE, PHOTO_AI_MODELS_DIR, PHOTO_AI_TILE,
PHOTO_AI_LOAD_ALL, PHOTO_AI_WARMUP, PHOTO_AI_FACE_MODEL,
PHOTO_AI_JPEG_QUALITY) пока не документированы в .env.example: compose их
ещё не подставляет, это Stage 2.

Найдено: на хосте есть GPU (nvidia-smi, драйвер 615.71.09), не хватает
только nvidia-container-toolkit. Установка — решение оператора, ветка CUDA
осталась непроверенной прогоном.
2026-09-29 00:36:16 +03:00
dev 88dbff0136 chore(photo-ai): Stage 0 закрыт, решения D1–D6 зафиксированы
Stage 0: чекбоксы отмечены, приёмка перепроверена — эталон воспроизводится
байт-в-байт (5/5 MATCH), /health -> {"ok":true}, сценарий «🤖 ИИ» -> done.

D1 — порт photo-ai на 127.0.0.1:8081 (loopback), применён и отражён в
README/.env.example.
D2 — отдельная photoAiHealth() вместо aiHealthCheck() в статусе заданий,
контракт service = {configured, reachable, latency_ms, error} + passthrough
полей photo-ai; правка и проверка в предыдущем коммите.
D3 — PHOTO_JOB_ACTIONS выносится на уровень модуля и используется в restore
и в валидации enhance-ai; CHECK в БД не добавляем (I6).
D4 — вендорить gfpgan/facexlib не нужно: пакеты есть на PyPI и уже в образе
(реalesrgan тянет их транзитивно). Зафиксированы проверенные URL весов и
расхождение: пин numpy<2 в Dockerfile не действует (в образе 2.2.6).
D5 — CodeFormer внедряется только при явной необходимости; на PyPI лишь
сторонняя обёртка, дефолт gfpgan, при отсутствии модуля -> 400 с текстом.
D6 — дефолт PHOTO_AI_URL не меняем (фото-ИИ включено из коробки на CPU).
2026-09-28 23:38:48 +03:00
dev 403574fe79 chore(photo-ai): раздел 0 — жёсткие инварианты I1–I7 зафиксированы
План фото-ИИ с восстановлением лиц разбит на этапы; этот коммит закрывает
раздел 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 на несуществующей записи (тест не создаёт реальных
заданий). Вместе с планом и чек-листом этапов.
2026-09-28 23:19:16 +03:00