Files
photo-editor/README.md

299 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Кадр — локальный AI-фоторедактор
Веб-приложение редактирует фотографии по текстовому промпту (через локальный
Codex CLI) и увеличивает разрешение через домашний сервер ComfyUI.
Поддерживаются фотографии JPEG, PNG, WebP и HEIC/HEIF. Файлы HEIC/HEIF
преобразуются на сервере в JPEG с помощью `heic-convert` до передачи в ComfyUI
или Codex.
## Как это работает
- **История версий**: каждая загрузка и каждый результат (редактирование или
апскейл) сохраняется в папке `history/` как отдельная версия. Можно вернуться
к любой версии кликом по ней в ленте «История версий» и продолжить правки
именно с неё — например, отредактировать фото, а затем увеличить разрешение
уже отредактированного варианта. История переживает перезапуск сервера и
обновление страницы; хранится последние 50 версий.
- **Редактирование**: сервер передаёт выбранную версию и промпт агенту Codex
CLI (`codex exec`). Агент редактирует изображение и сохраняет результат.
Обработка обычно занимает 1–3 минуты.
- **Увеличение разрешения**: 4× upscale через ComfyUI моделью
`RealESRGAN_x4plus.safetensors` — применяется к выбранной (в том числе
отредактированной) версии.
Редактирование и апскейл выполняются в фоне: `POST /api/edit` и
`POST /api/upscale` отвечают мгновенно с идентификатором задания (`jobId`),
а тяжёлая работа продолжается на сервере. Фронтенд опрашивает
`GET /api/jobs/:id/status` каждые 3 секунды; страницу можно закрыть —
готовая версия появится в истории.
- **Дерево истории**: вместо плоской ленты версии показаны деревом — каждая
исходная фотография образует группу со своей веткой (исходник → правки →
апскейлы), потомки выстраиваются по `parentId` с отступами, а сироты
(версии, чей родитель уже удалён или оттеснён лимитом) показываются как
отдельные корни. Ветку можно свернуть, под каждой edit-версией виден её
промпт (обрезка до двух строк, полный текст — в подсказке).
- **Удаление версий**: на узле (при наведении) есть кнопка «Удалить» —
версия удаляется вместе со всеми потомками и их файлами
(`DELETE /api/history/:id`, каскадно по `parentId`).
- **Мультизагрузка**: в поле «Фотография» можно выбрать сразу несколько
файлов — каждый становится отдельной исходной версией (своей группой),
импорт идёт последовательно с прогрессом «Загрузка N из M…».
- **Редактирование с референсом и областью**: перед правкой можно задать
референс — файл или версию из истории. На превью референса рисуется
прямоугольник области; сервер передаёт Codex обе картинки (повторяемый
флаг `-i`) и кладёт координаты области в промпт. Если область не выбрана,
референс всё равно передаётся как ориентир по сюжету и ракурсу.
- **Подсказки промптов**: кнопка «💡 Предложить промпт» — мгновенные
варианты из истории (`GET /api/suggest-prompts`), а «Спросить Codex» —
асинхронный джоб `POST /api/suggest` (тот же механизм опроса статуса, что
и у редактирования).
## Требования
- Node.js 20 или новее
- Локальный Codex CLI (`codex`) с выполненным входом в аккаунт OpenAI
- Доступ к ComfyUI: `http://192.168.31.240:8188`
- Checkpoint: `sdxl_turbo.safetensors`
- Upscale-модель: `RealESRGAN_x4plus.safetensors`
- Установленные зависимости проекта, включая `heic-convert` (ставится
командой `npm install`)
## Запуск
```powershell
npm install
npm start
```
После запуска редактор закрыт cookie-сессией. По умолчанию логин — `admin`,
а пароль генерируется при старте сервера и печатается в консоль (баннер
`APP_PASSWORD не задан. Сгенерирован случайный пароль`). Чтобы зафиксировать
учётные данные, задайте переменные окружения `APP_USER` / `APP_PASSWORD`.
Подробнее — в разделе «Авторизация».
## Авторизация
Редактор «Кадр» закрыт cookie-сессией: все `/api/*` эндпоинты, кроме
`/api/health`, `/api/login`, `/api/me`, `/api/logout`, требуют валидную
сессию, иначе возвращают `401 {"ok":false,"error":"Требуется авторизация."}`.
На главной странице поверх редактора показывается форма входа, которая
блокирует работу, пока владелец не войдёт. Статические файлы и `/api/health`
остаются публичными.
Учётные данные задаются переменными окружения:
- `APP_USER` — логин владельца (по умолчанию `admin`).
- `APP_PASSWORD` — пароль. Если не задан или пуст, сервер генерирует
случайный 16-символьный пароль при старте и печатает его в консоль/лог
контейнера (баннер `APP_PASSWORD не задан. Сгенерированный пароль`).
Сессия — cookie `kadr_session`:
- срок жизни 7 суток;
- скользящая (sliding): продлевается при каждой активности;
- `HttpOnly`, `SameSite=Lax`, без `Secure` (приложение работает по HTTP
в локальной сети);
- сессии хранятся в памяти сервера — перезапуск сбрасывает все входы.
Публичные эндпоинты: `GET /api/health`, `POST /api/login`,
`GET /api/me`, `POST /api/logout`. Вход в систему — `POST /api/login`
с JSON-телом `{"username": "...", "password": "..."}`.
Остальные эндпоинты (все требуют авторизации): `GET /api/history`,
`POST /api/history/import`, `DELETE /api/history/:id`,
`GET /api/suggest-prompts`, `POST /api/suggest`, `POST /api/preview`,
`POST /api/edit`, `POST /api/upscale`, `GET /api/jobs/:id/status`.
## Логирование
Все действия сервера пишутся в стандартный вывод контейнера — смотрите их в
**Portainer → Containers → kadr-photo-editor → Logs**.
Что логируется:
- каждый запрос к `/api/*` (метод, путь, HTTP-статус, время обработки);
- старт сервера (порт, логин, адрес ComfyUI);
- вход и выход пользователя;
- импорт фотографии;
- редактирование: запуск задания, успешное завершение или ошибка;
- апскейл: запуск задания, успешное завершение или ошибка;
- ошибки обработки запросов (с кодом статуса и текстом ошибки).
Что **не** логируется: пароли, cookie `kadr_session`, содержимое фотографий.
Единственное исключение — баннер сгенерированного пароля при первом старте
(когда `APP_PASSWORD` не задан): пароль печатается в лог один раз, чтобы
владелец мог войти.
Уровень логирования задаётся переменной окружения `LOG_LEVEL`:
- `debug` — все сообщения, включая `GET /api/health` и опросы статуса заданий
`GET /api/jobs/:id/status`;
- `info` (по умолчанию) — основные события;
- `warn` — предупреждения;
- `error` — только ошибки.
Примеры строк лога:
```
2026-08-26T06:00:00.000Z [INFO] Сервер запущен {"port":3000,"user":"admin","comfyUrl":"http://192.168.31.240:8188","passwordGenerated":false}
2026-08-26T06:00:01.000Z [INFO] GET /api/history -> 200 2ms
2026-08-26T06:00:05.000Z [INFO] Вход выполнен {"user":"admin"}
2026-08-26T06:00:10.000Z [INFO] Редактирование запущено {"jobId":"...","sourceId":"...","prompt":"..."}
2026-08-26T06:02:30.000Z [INFO] Редактирование завершено {"jobId":"...","ok":true,"exitCode":0,"timedOut":false,"durationMs":140000,"versionId":"...","lastMessage":"..."}
2026-08-26T06:02:31.000Z [WARN] Ошибка обработки запроса {"status":401,"message":"Требуется авторизация."}
```
## Docker
Образ собирается на базе `node:22-bookworm-slim` (Debian, glibc) — Codex CLI
распространяется как бинарник под glibc и не работает на Alpine/musl.
```powershell
# сборка и запуск
docker compose up -d --build
# или вручную:
docker build -t kadr-photo-editor .
docker run -d --name kadr-photo-editor -p 3000:3000 ^
-v "$env:USERPROFILE\.codex:/root/.codex" ^
-v "${PWD}\history:/app/history" ^
-e COMFY_URL=http://192.168.31.240:8188 ^
-e APP_USER=admin ^
-e APP_PASSWORD=your-strong-password ^
kadr-photo-editor
```
Что важно знать:
- **Авторизация Codex**: контейнер монтирует `~/.codex` хоста в `/root/.codex`
(auth + config + навыки). Альтернатива — переменная окружения
`OPENAI_API_KEY`.
- **История версий**: каталог `history/` смонтирован томом, версии переживают
пересоздание контейнера.
- **ComfyUI**: задаётся через `COMFY_URL` (по умолчанию
`http://192.168.31.240:8188`); из контейнера адрес доступен как исходящий
адрес локальной сети.
- **Порт**: по умолчанию 3000, меняется через `PORT`.
## Сборка образа в Gitea (Actions)
Репозиторий: `https://git.byte-mate.ru/Coder/photo-editor` (ветка `main`).
Workflow лежит в `.gitea/workflows/build.yml` и запускается **вручную**:
вкладка **Actions** → **Run workflow** → выбрать ветку → нажать кнопку.
После успешного прогона образ публикуется во встроенный registry Gitea:
```powershell
docker pull git.byte-mate.ru/coder/photo-editor:latest
docker run -d --name kadr-photo-editor -p 3000:3000 ^
-v "$env:USERPROFILE\.codex:/root/.codex" ^
-v "${PWD}\history:/app/history" ^
-e COMFY_URL=http://192.168.31.240:8188 ^
-e APP_USER=admin ^
-e APP_PASSWORD=your-strong-password ^
git.byte-mate.ru/coder/photo-editor:latest
```
Требования для работы кнопки:
1. **Actions включены** на инстансе (уже включены).
2. **Зарегистрирован runner** (`act_runner`) — уже работает
(«Runner-10C-32Gb»).
3. **Секрет `GIT_PASSWORD`** в репозитории (Settings → Actions → Secrets):
Personal Access Token с правами на пакеты. Имена `GITEA_TOKEN` /
`GITHUB_TOKEN` зарезервированы Gitea и не создаются.
4. Образ публикуется в `git.byte-mate.ru/coder/photo-editor` (теги `latest` и
имя ветки; путь в registry обязательно в нижнем регистре).
Если runner ещё не зарегистрирован:
```bash
# токен регистрации: Администрирование → Actions → Runners → Create new runner
docker run -d --name gitea-runner --restart always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v gitea-runner:/data \
-e GITEA_INSTANCE_URL=https://git.byte-mate.ru \
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token> \
gitea/act_runner:latest
```
3. После регистрации нажмите «Run workflow» — образ соберётся Buildx'ом и
уедет в registry.
## Развёртывание в Portainer (workflow deploy-portainer)
Workflow `.gitea/workflows/deploy.yml` разворачивает сервис на
[Portainer](https://ptr.byte-mate.ru): создаёт или обновляет стек
`photo-editor` из `deploy/docker-compose.yml` через API Portainer.
Запуск: **Actions → Run workflow** (workflow **deploy-portainer**).
Требования:
1. **Секрет `PORTAINER_API_KEY`** (Settings → Actions → Secrets) — API-ключ
Portainer: **Account → API keys → Add key**. Внимание: Portainer 2.39
принимает ключ только через заголовок `X-API-Key` (не Bearer) — workflow
это уже учитывает.
2. **Переменная `PORTAINER_ENDPOINT_ID`** (Settings → Actions → Variables) —
id окружения Docker в Portainer. Если не задана, берётся первый из
`/api/endpoints`. Сейчас endpoint — `server1`, id **3**.
3. **Registry в Portainer** (Registries → Add registry): URL
`git.byte-mate.ru`, username `Coder`, пароль — Personal Access Token.
Без этого хост не сможет стянуть образ (registry приватный, анонимный
pull возвращает 401).
4. На хосте Portainer должен существовать каталог `/root/.codex` с
аутентификацией Codex (auth.json + config.toml) — он монтируется в
контейнер. Альтернатива — переменная `OPENAI_API_KEY` в
`deploy/docker-compose.yml`.
5. На хосте должен работать прокси `http://192.168.31.240:4067`: прямой
выход сервера блокируется OpenAI (403, cf-ray HEL), поэтому Codex ходит
в ChatGPT API через прокси. Переменные `HTTP(S)_PROXY` и `NO_PROXY`
прописаны в `deploy/docker-compose.yml` (внутренние адреса — ComfyUI,
свой хост — идут напрямую).
6. **Авторизация владельца**: секреты `APP_USER` / `APP_PASSWORD`
(Settings → Actions → Secrets) передаются workflow deploy-portainer
в переменные окружения стека. Если секреты не заданы (пустые значения),
сервер использует дефолты: логин `admin` и сгенерированный при старте
пароль, напечатанный в лог контейнера. Альтернатива — задать переменные
вручную в Portainer UI: стек `photo-editor` → **Edit** → **Environment
variables** → `APP_USER` / `APP_PASSWORD`.
Поведение: если стека нет — создаёт; если есть — обновляет с
`RepullImageAndRedeploy=true` (всегда перетягивает свежий образ). Контейнер
`kadr-photo-editor` слушает порт **3000** на хосте Portainer; история версий
хранится в именованном томе `kadr-history` и переживает обновления.
## Тест скорости моделей Codex (ветка testing)
Сервер умеет переопределять модель и уровень рассуждений через переменные
окружения (без правки `~/.codex/config.toml`):
```powershell
$env:CODEX_MODEL = "gpt-5.6-luna"; $env:CODEX_REASONING_EFFORT = "low"
npm start
```
Проверено на ветке `testing`: одинаковое фото 512×512 и один промпт,
замер полного цикла редактирования:
| Конфигурация | Время | Токены |
|---|---|---|
| `gpt-5.6-sol` + effort high (как сейчас) | 73.9 s | 15 286 |
| `gpt-5.6-sol` + effort low | 61.7 s | 29 760 |
| `gpt-5.6-luna` (default medium) | 68.6 s | 52 005 |
| `gpt-5.4-mini` (default medium) | 82.1 s | 19 437 |
| `gpt-5.2-codex`, `gpt-5.2-codex-mini` | — | недоступны для ChatGPT-аккаунта (API 400) |
Выводы:
- **Выбор модели почти не влияет на время** (60–82 s): узкое место — вызов
image-модели при редактировании, а не агентная модель.
- Единственный заметный выигрыш — `CODEX_REASONING_EFFORT=low` (~17%).
Кстати, у `gpt-5.6-sol` low — это значение по умолчанию; в
`~/.codex/config.toml` оно переопределено на `high`, поэтому редактирование
такое долгое.
- Для радикального ускорения нужно уменьшать разрешение исходника для шага
редактирования (медленнее при 1200×800, чем при 512×512) — но тогда
результат будет меньшего размера.