# Кадр — локальный 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= \ 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) — но тогда результат будет меньшего размера.