From 88dbff01363a937707b5bd8bdfa2130a7b865186 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 28 Sep 2026 23:38:48 +0300 Subject: [PATCH] =?UTF-8?q?chore(photo-ai):=20Stage=200=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=D1=82,=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20D1=E2=80=93D6=20=D0=B7=D0=B0=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- TODO_PHOTO_FACE_AI.md | 139 ++++++++++++++++++++++++++++++++---------- 1 file changed, 108 insertions(+), 31 deletions(-) diff --git a/TODO_PHOTO_FACE_AI.md b/TODO_PHOTO_FACE_AI.md index e316c3e..d4b3e99 100644 --- a/TODO_PHOTO_FACE_AI.md +++ b/TODO_PHOTO_FACE_AI.md @@ -62,7 +62,7 @@ |---|---|---| | `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:194–204` | `photo-ai`: `MODEL_PATH`, `MAX_INPUT_PIXELS`, том `photo-ai-models`, `ports: 127.0.0.1:8081:8080` (D1) | новые 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 **нет** | миграция схемы не требуется | @@ -96,44 +96,120 @@ ## 2. Расхождения плана с кодом — решить ДО правок -- [ ] **D1. Порт для ручных проверок.** `photo-ai` не имеет `ports`/`expose`, а хостовый `8080` занят +Все шесть расхождений закрыты 2026-09-28 (решения ниже, правки кода — в своих этапах). + +- [x] **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` по умолчанию + в `text-corrector`. Решение выбрано одно: пробросить `ports: ["127.0.0.1:8081:8080"]` в сервис + `photo-ai` — только loopback, наружу (`0.0.0.0`) ничего не публикуется, хостовый `8080` + (`text-corrector`) не затрагивается. **Правка применена 2026-09-28:** порт добавлен в + `docker-compose.yml`, способ отражён в `README.md` (раздел «ИИ-улучшение фото») и `.env.example`. + Проверено: `docker compose up -d photo-ai` → `curl http://127.0.0.1:8081/health` → `{"ok":true}`, + `ss -tulpn` → `LISTEN 127.0.0.1:8081` (не `0.0.0.0`). + +- [x] **D2. `service` в статусе фото-воркера.** `server.js:5825` вызывает `aiHealthCheck()` (это health + **текстового** ИИ), результат не возвращается. Решение: отдельная функция, общий `aiHealthCheck()` + не переиспользуем и из `/api/photo-jobs/status` убираем. + - `photoAiHealth()` рядом с `aiHealthCheck()`: `GET ${PHOTO_AI_URL}/health`, таймаут 5 с, без throw; + пустой `PHOTO_AI_URL` → `{ configured: false, reachable: false, latency_ms: 0, error: 'PHOTO_AI_URL не настроен' }`, + недоступность/таймаут → `{ configured: true, reachable: false, latency_ms, error }`. + - `service` в ответе = `{ configured, reachable, latency_ms, error }` + passthrough полей photo-ai + (`ok, ready, device, device_name, half, tile, driver, cuda, vram_total_mb, vram_free_mb, models, + face_models, loaded, loading, max_pixels`). Первые четыре — тот же контракт, который уже читает + фронт текстового ИИ (`public/js/worker.js:89–116`: `reachable`, `latency_ms`, `error`), + поэтому карточки переиспользуются без правок. + - Вызов уходит в тот же `Promise.all`, что и запросы к БД, — иначе статус получает лишние 5 с; + HTTP 200 в любом случае, недоступный сервис — не ошибка API. + - Прямой прокси для оператора — `GET /api/photo-ai/health` (`requireAdmin`), отдельным пунктом Stage 4. + - **Правка применена 2026-09-28:** `photoAiHealth()` в `server.js` рядом с `aiHealthCheck()`, + вызов ушёл в `Promise.all` запроса `/api/photo-jobs/status`, `service` возвращается. + Проверено на живом стеке: сервис поднят → `{"ok":true,"configured":true,"reachable":true,"latency_ms":3,"error":null}`; + `docker compose stop photo-ai` → HTTP 200 и `{"configured":true,"reachable":false,"latency_ms":3661,"error":"fetch failed"}` + (без исключения); после `start` → снова `reachable: true`. Контракт зафиксирован в + `api.smoketest.js` (два новых assert'а). + +- [x] **D3. Где живёт белый список действий.** План говорит про два места (`ensurePhotoJobsTable()` и restore). + По факту `PHOTO_JOB_ACTIONS` используется **только** в нормализации restore-данных (`server.js:2254`, + единственное применение — `server.js:2259`; `grep` по репозиторию больше нигде), а в + `ensurePhotoJobsTable()` (`server.js:1237–1254`) CHECK-ограничения на `action` нет: + `action VARCHAR(20) NOT NULL DEFAULT 'ai'` — новые значения (`ai_face`, `ai_upscale`) помещаются. + Решение: обновить одно место. + - Набор выносится на уровень модуля (рядом с `PHOTO_AI_URL`, `server.js:1841`), а не внутрь + функции restore; используется в restore-нормализации **и** в валидации + `POST /api/entries/:id/photo/enhance-ai` (Stage 4) — забыть маршрут нельзя. + - CHECK в БД **не добавляем** (I6: никаких изменений схемы, миграция не нужна). Сверка списка + с фактическими `action`, которые пишет код, — `grep` на приёмке этапа. + - `ai_upscale` в список попадает сразу, хотя маршрута, его создающего, пока нет: список — + это допустимые значения для restore, а не реестр маршрутов. + +- [x] **D4. Доступность пакетов.** Проверено 2026-09-28 в работающем контейнере `photo-ai` + (`pip download gfpgan==1.3.8 facexlib==0.3.0 --no-deps` — оба колеса с PyPI, 52 и 59 КБ; + `pip list` в образе: `gfpgan 1.3.8`, `facexlib 0.3.0`, `basicsr 1.4.2`, `realesrgan 0.3.0`, + `filterpy 1.4.5`, `numba 0.67.0`, `lmdb 2.3.0`, `scipy 1.15.3`; `import gfpgan, facexlib` — ок). + Решение: **вендорить не нужно**, пакеты есть на PyPI и уже стоят в образе — `realesrgan==0.3.0` + тянет `gfpgan>=1.3.5` и `facexlib>=0.2.5` транзитивно. CodeFormer официальным pip-пакетом + не распространяется (см. D5). + - Stage 2 фиксирует версии явно (`gfpgan==1.3.8 facexlib==0.3.0`) и убирает dev-зависимости + gfpgan из рантайма (`tb-nightly`, `yapf`): ставить gfpgan с `--no-deps` и перечислить + реальные зависимости явно. + - **Найденное расхождение (Stage 2):** пин `numpy<2` в `photo-ai/Dockerfile` не действует — в образе + `numpy 2.2.6`, потому что пин живёт в отдельном вызове `pip install`, который следующие установки + не учитывают; там же одновременно стоят `opencv-python 5.0.0.93` (через gfpgan) и + `opencv-python-headless 5.0.0.93`. До Stage 2 numpy не трогаем (Stage 1 меряет I1 на текущем + 2.2.6); в Stage 2 пин либо переносится в один общий вызов `pip install`, либо фиксируется + фактическая версия — с обязательной перепроверкой эталона I1 после любого изменения numpy. + - Веса (проверено HEAD): `x2plus` v0.2.1 — 200; `general-x4v3` и `animevideov3` v0.2.5.0 — 200; + `codeformer.pth` v0.1.0 — 200. GFPGAN: берём `GFPGANv1.4.pth` из релиза `v1.3.0` (200, `arch='clean'`, + `channel_multiplier=2` — как у официального `inference_gfpgan.py -v 1.4`); `GFPGANCleanv1-NoCE-C2.pth` + в релизах `v1.3.8`/`v1.3.4`/`v1.3.0` отсутствует (404), в `v0.2.0` есть (200) — как запасной вариант. + - **Учтётся в Stage 1:** `GFPGANer` жёстко передаёт facexlib `model_rootpath='gfpgan/weights'` + (относительный путь → каталог образа, не том `/models`), поэтому веса детектора/парсера facexlib + сейчас скачиваются мимо тома и теряются при пересборке. Свой `FaceRestoreHelper`/каталог + `/models/weights` — обязательное требование этапа. + +- [x] **D5. CodeFormer — опционален по умолчанию.** Проверено 2026-09-28: на PyPI есть только + сторонняя обёртка `codeformer 0.0.11` (`github.com/rohitkhatri/codeformer`, тянет `lpips`) — + это не официальный `sczhou/CodeFormer`, использовать его не будем. Официальные веса доступны. + Решение: официальный модуль вендорится в `photo-ai/vendor/codeformer/` **только если** face-режим + CodeFormer реально понадобится; в рамках текущего плана не вендорим, дефолт + `PHOTO_AI_FACE_MODEL=gfpgan` (Stage 1–3), чтобы дефолтный путь работал без вендоринга. + - `FACE_REGISTRY` всегда содержит обе записи, но запись `codeformer` активна только если + `import codeformer` (с `photo-ai/vendor` в `sys.path`) успешен. + - Нет модуля → запись не попадает в `face_models` в `/health` и в список `/models`, UI её не показывает, + `POST /enhance` с `face_model=codeformer` → `400` с текстом «CodeFormer не установлен в образ, + доступен gfpgan». Сборка и CPU-режим не падают. + - `strength` валиден только для CodeFormer: при `face_model=gfpgan` и `strength`, отличном от 0.7, + → `400` с пояснением, иначе параметр молча игнорировался бы. + - Веса CodeFormer качаются тем же загрузчиком в `/models/weights/codeformer.pth`. + +- [x] **D6. Поведение без `photo-ai` в compose.** Сейчас `PHOTO_AI_URL` по умолчанию `http://photo-ai:8080` (`docker-compose.yml:79`) — то есть «ИИ» включён по умолчанию. - Не менять дефолт молча; если меняется — явно записать в `.env.example` и `README`. + Решение: дефолт **не меняем** — фото-ИИ включено из коробки и работает на CPU; выключается + только явно (`PHOTO_AI_URL=` в `.env` → кнопка «🤖 ИИ» скрыта, `enhance-ai` → 503, I3). + Заодно исправлен найденный рассинхрон: `.env.example` содержал `PHOTO_AI_URL=` (пусто) с комментарием + «пусто = контейнер photo-ai», то есть инструкция «скопируй `.env.example`» молча выключала ИИ-фото. + В этом же изменении `.env.example` приведён к дефолту compose с явным описанием обоих состояний; + блок про photo-ai в `README` (включая GPU-запуск) появится в Stage 6 с той же формулировкой. + Текущий `.env` переменной не содержит → на этом хосте действует дефолт compose (включено). --- ## 3. Stage 0. Подготовка и эталон «до» (обязательно до любых правок кода) -- [ ] Зафиксировать окружение: `docker --version`, `docker compose version`, `nvidia-smi`, +- [x] Зафиксировать окружение: `docker --version`, `docker compose version`, `nvidia-smi`, наличие/отсутствие `nvidia-ctk`, `docker info | grep -i runtime`. -- [ ] Если `nvidia-ctk` нет — зафиксировать это как «GPU-режим не проверяем на этом хосте», +- [x] Если `nvidia-ctk` нет — зафиксировать это как «GPU-режим не проверяем на этом хосте», **не** пытаться ставить системные пакеты без явного разрешения оператора. -- [ ] Сохранить текущее состояние `.env` (`PHOTO_AI_URL` пусто или задан). -- [ ] Поднять текущий стек как есть: `docker compose up -d --build`. -- [ ] Снять эталон «до» на 3–5 фото (портрет, групповое, без лиц, зашумлённое 640×480): +- [x] Сохранить текущее состояние `.env` (`PHOTO_AI_URL` пусто или задан). +- [x] Поднять текущий стек как есть: `docker compose up -d --build`. +- [x] Снять эталон «до» на 3–5 фото (портрет, групповое, без лиц, зашумлённое 640×480): результат `/enhance` с `scale=2` без других параметров + `/health`. Сохранить файлы и размеры в `backups/photo-face-ai-baseline/` (вне git). -- [ ] **Приёмка:** эталон сохранён, `curl /health` отвечает, текущий сценарий «🤖 ИИ» даёт `done`. +- [x] **Приёмка:** эталон сохранён, `curl /health` отвечает, текущий сценарий «🤖 ИИ» даёт `done`. + +Stage 0 закрыт 2026-09-28, журнал проверок — «Журнал раздела 0» выше. Повторная сверка при закрытии: +`node backups/photo-face-ai-baseline/verify.js after-section0` → 5/5 `MATCH` (I1 байт-в-байт), +`photo-ai /health` → `{"ok":true}` из контейнера `app`, сценарий «🤖 ИИ» → `done` (I2). --- @@ -185,7 +261,7 @@ - [ ] `ENV PHOTO_AI_MODELS_DIR=/models`; `MODEL_PATH` остаётся валидным алиасом. - [ ] `docker-compose.yml`, сервис `photo-ai`: env `PHOTO_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`. + `start_period: 300s`; порт по `D1` уже проброшен на loopback — сохранить. - [ ] Новый `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`). Без него стек поднимается на любой машине. @@ -239,9 +315,10 @@ **Пустое тело → сегодняшнее поведение** (`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` +- [x] `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. + `loaded`). Недоступен → `{reachable:false}`, ответ 200. — **сделано 2026-09-28** (контракт и проверка + в `D2`; поля `device`/`device_name`/`vram_*` появятся вместе с расширенным `/health` в Stage 1) - [ ] `GET /api/photo-ai/health` (`requireAdmin`) — прямой прокси `/health` photo-ai для оператора. - [ ] `getStackInfo()` (`server.js:769`) — блок `photo_ai` (`engine: 'Real-ESRGAN + GFPGAN'`, `driver`, `device`, `device_name`, `vram_total_mb`, `models`). Без credentials, только hostname.