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:
+108
-31
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user