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).
This commit is contained in:
dev
2026-09-28 23:38:48 +03:00
parent 00fbf41efb
commit 88dbff0136
+108 -31
View File
@@ -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/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 | | `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` | без изменений | | `docker-compose.yml:208` | том `photo-ai-models` | без изменений |
| `server.js:1841` | `PHOTO_AI_URL` | + константы `PHOTO_AI_FACE_MODEL`, `PHOTO_AI_FACE_TIMEOUT_MS` | | `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:1237–1256` | `ensurePhotoJobsTable()`: колонка `action VARCHAR(20)`, `params JSONB`, CHECK на action **нет** | миграция схемы не требуется |
@@ -96,44 +96,120 @@
## 2. Расхождения плана с кодом — решить ДО правок ## 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`. Команды вида `curl http://localhost:8080/enhance` из §10 плана попадут
в `text-corrector`. Решение: пробросить `ports: ["127.0.0.1:8081:8080"]` в сервис `photo-ai` в `text-corrector`. Решение выбрано одно: пробросить `ports: ["127.0.0.1:8081:8080"]` в сервис
(только loopback — правило «не публиковать наружу» соблюдено) **или** выполнять ручные проверки `photo-ai` — только loopback, наружу (`0.0.0.0`) ничего не публикуется, хостовый `8080`
через `docker compose exec -T app node -e ...`. Выбрать одно и применить; в `README` и `.env.example` (`text-corrector`) не затрагивается. **Правка применена 2026-09-28:** порт добавлен в
отразить выбранный способ. `docker-compose.yml`, способ отражён в `README.md` (раздел «ИИ-улучшение фото») и `.env.example`.
- [ ] **D2. `service` в статусе фото-воркера.** `server.js:5825` вызывает `aiHealthCheck()` (это health Проверено: `docker compose up -d photo-ai` → `curl http://127.0.0.1:8081/health` → `{"ok":true}`,
**текстового** ИИ), результат не возвращается. Нужен отдельный `photoAiHealth()` с таймаутом 5 с, `ss -tulpn` → `LISTEN 127.0.0.1:8081` (не `0.0.0.0`).
обращающийся к `${PHOTO_AI_URL}/health`, и его результат в ответе `/api/photo-jobs/status`.
При недоступности — `{ reachable: false }`, без исключения. - [x] **D2. `service` в статусе фото-воркера.** `server.js:5825` вызывает `aiHealthCheck()` (это health
- [ ] **D3. Где живёт белый список действий.** План говорит про два места (`ensurePhotoJobsTable()` и restore). **текстового** ИИ), результат не возвращается. Решение: отдельная функция, общий `aiHealthCheck()`
По факту `PHOTO_JOB_ACTIONS` используется **только** в нормализации restore-данных (`server.js:2254`), не переиспользуем и из `/api/photo-jobs/status` убираем.
а в `ensurePhotoJobsTable()` CHECK-ограничения на `action` нет. Обновить одно место; при желании - `photoAiHealth()` рядом с `aiHealthCheck()`: `GET ${PHOTO_AI_URL}/health`, таймаут 5 с, без throw;
вынести набор на уровень модуля, чтобы его нельзя было забыть. пустой `PHOTO_AI_URL` → `{ configured: false, reachable: false, latency_ms: 0, error: 'PHOTO_AI_URL не настроен' }`,
- [ ] **D4. Доступность пакетов.** Проверить `pip index`/зеркало наличие `gfpgan` и `facexlib` недоступность/таймаут → `{ configured: true, reachable: false, latency_ms, error }`.
(`pip download gfpgan==1.3.8 facexlib==0.3.0 -d /tmp/x --no-deps`). Если недоступны — вендорить - `service` в ответе = `{ configured, reachable, latency_ms, error }` + passthrough полей photo-ai
в `photo-ai/vendor/` и копировать в образ. CodeFormer официальным pip-пакетом не распространяется. (`ok, ready, device, device_name, half, tile, driver, cuda, vram_total_mb, vram_free_mb, models,
- [ ] **D5. CodeFormer — опционален по умолчанию.** Реализовать как запись реестра, которая включается, face_models, loaded, loading, max_pixels`). Первые четыре — тот же контракт, который уже читает
только если вендоренный модуль импортируется. Если нет: `face_models` в `/health` его не содержит, фронт текстового ИИ (`public/js/worker.js:89–116`: `reachable`, `latency_ms`, `error`),
API отвечает `400` с понятным текстом, UI не показывает его в списке. Сборка и CPU-режим не падают. поэтому карточки переиспользуются без правок.
- [ ] **D6. Поведение без `photo-ai` в compose.** Сейчас `PHOTO_AI_URL` по умолчанию - Вызов уходит в тот же `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`) — то есть «ИИ» включён по умолчанию. `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. Подготовка и эталон «до» (обязательно до любых правок кода) ## 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`, `docker info | grep -i runtime`.
- [ ] Если `nvidia-ctk` нет — зафиксировать это как «GPU-режим не проверяем на этом хосте», - [x] Если `nvidia-ctk` нет — зафиксировать это как «GPU-режим не проверяем на этом хосте»,
**не** пытаться ставить системные пакеты без явного разрешения оператора. **не** пытаться ставить системные пакеты без явного разрешения оператора.
- [ ] Сохранить текущее состояние `.env` (`PHOTO_AI_URL` пусто или задан). - [x] Сохранить текущее состояние `.env` (`PHOTO_AI_URL` пусто или задан).
- [ ] Поднять текущий стек как есть: `docker compose up -d --build`. - [x] Поднять текущий стек как есть: `docker compose up -d --build`.
- [ ] Снять эталон «до» на 3–5 фото (портрет, групповое, без лиц, зашумлённое 640×480): - [x] Снять эталон «до» на 3–5 фото (портрет, групповое, без лиц, зашумлённое 640×480):
результат `/enhance` с `scale=2` без других параметров + `/health`. Сохранить файлы и результат `/enhance` с `scale=2` без других параметров + `/health`. Сохранить файлы и
размеры в `backups/photo-face-ai-baseline/` (вне git). размеры в `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` остаётся валидным алиасом. - [ ] `ENV PHOTO_AI_MODELS_DIR=/models`; `MODEL_PATH` остаётся валидным алиасом.
- [ ] `docker-compose.yml`, сервис `photo-ai`: env `PHOTO_AI_DEVICE`, `PHOTO_AI_FACE_MODEL`, `PHOTO_AI_TILE`, - [ ] `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` с `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`, - [ ] Новый `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`). `PHOTO_AI_DEVICE=cuda`, `deploy.resources.reservations.devices` (`driver: nvidia`, `count: 1`).
Без него стек поднимается на любой машине. Без него стек поднимается на любой машине.
@@ -239,9 +315,10 @@
**Пустое тело → сегодняшнее поведение** (`params = NULL`, `action='ai'`) — I2. **Пустое тело → сегодняшнее поведение** (`params = NULL`, `action='ai'`) — I2.
- [ ] `PHOTO_JOB_ACTIONS` (`server.js:2254`): + `'ai_face'`, `'ai_upscale'` (см. `D3`). Схема - [ ] `PHOTO_JOB_ACTIONS` (`server.js:2254`): + `'ai_face'`, `'ai_upscale'` (см. `D3`). Схема
`action VARCHAR(20)` вмещает новые значения — миграция не нужна. `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`, (`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 для оператора. - [ ] `GET /api/photo-ai/health` (`requireAdmin`) — прямой прокси `/health` photo-ai для оператора.
- [ ] `getStackInfo()` (`server.js:769`) — блок `photo_ai` (`engine: 'Real-ESRGAN + GFPGAN'`, `driver`, - [ ] `getStackInfo()` (`server.js:769`) — блок `photo_ai` (`engine: 'Real-ESRGAN + GFPGAN'`, `driver`,
`device`, `device_name`, `vram_total_mb`, `models`). Без credentials, только hostname. `device`, `device_name`, `vram_total_mb`, `models`). Без credentials, только hostname.