Кадр — локальный 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 секунды; страницу можно закрыть —
готовая версия появится в истории.
Требования
- 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)
Запуск
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": "..."}.
Логирование
Все действия сервера пишутся в стандартный вывод контейнера — смотрите их в 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.
# сборка и запуск
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:
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
Требования для работы кнопки:
- Actions включены на инстансе (уже включены).
- Зарегистрирован runner (
act_runner) — уже работает («Runner-10C-32Gb»). - Секрет
GIT_PASSWORDв репозитории (Settings → Actions → Secrets): Personal Access Token с правами на пакеты. ИменаGITEA_TOKEN/GITHUB_TOKENзарезервированы Gitea и не создаются. - Образ публикуется в
git.byte-mate.ru/coder/photo-editor(тегиlatestи имя ветки; путь в registry обязательно в нижнем регистре).
Если runner ещё не зарегистрирован:
# токен регистрации: Администрирование → 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
- После регистрации нажмите «Run workflow» — образ соберётся Buildx'ом и уедет в registry.
Развёртывание в Portainer (workflow deploy-portainer)
Workflow .gitea/workflows/deploy.yml разворачивает сервис на
Portainer: создаёт или обновляет стек
photo-editor из deploy/docker-compose.yml через API Portainer.
Запуск: Actions → Run workflow (workflow deploy-portainer).
Требования:
- Секрет
PORTAINER_API_KEY(Settings → Actions → Secrets) — API-ключ Portainer: Account → API keys → Add key. Внимание: Portainer 2.39 принимает ключ только через заголовокX-API-Key(не Bearer) — workflow это уже учитывает. - Переменная
PORTAINER_ENDPOINT_ID(Settings → Actions → Variables) — id окружения Docker в Portainer. Если не задана, берётся первый из/api/endpoints. Сейчас endpoint —server1, id 3. - Registry в Portainer (Registries → Add registry): URL
git.byte-mate.ru, usernameCoder, пароль — Personal Access Token. Без этого хост не сможет стянуть образ (registry приватный, анонимный pull возвращает 401). - На хосте Portainer должен существовать каталог
/root/.codexс аутентификацией Codex (auth.json + config.toml) — он монтируется в контейнер. Альтернатива — переменнаяOPENAI_API_KEYвdeploy/docker-compose.yml. - На хосте должен работать прокси
http://192.168.31.240:4067: прямой выход сервера блокируется OpenAI (403, cf-ray HEL), поэтому Codex ходит в ChatGPT API через прокси. ПеременныеHTTP(S)_PROXYиNO_PROXYпрописаны вdeploy/docker-compose.yml(внутренние адреса — ComfyUI, свой хост — идут напрямую). - Авторизация владельца: секреты
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):
$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-sollow — это значение по умолчанию; в~/.codex/config.tomlоно переопределено наhigh, поэтому редактирование такое долгое. - Для радикального ускорения нужно уменьшать разрешение исходника для шага редактирования (медленнее при 1200×800, чем при 512×512) — но тогда результат будет меньшего размера.