Кадр — локальный AI-фоторедактор

Веб-приложение редактирует фотографии по текстовому промпту (через локальный Codex CLI) и увеличивает разрешение через домашний сервер ComfyUI.

Поддерживаются фотографии JPEG, PNG, WebP и HEIC/HEIF. Файлы HEIC/HEIF преобразуются на сервере в JPEG с помощью heic-convert до передачи в ComfyUI или Codex.

Как это работает

  • История версий: каждая загрузка и каждый результат (редактирование или апскейл) сохраняется в папке history/ как отдельная версия. Можно вернуться к любой версии кликом по ней в ленте «История версий» и продолжить правки именно с неё — например, отредактировать фото, а затем увеличить разрешение уже отредактированного варианта. История переживает перезапуск сервера и обновление страницы; хранится последние 50 версий.
  • Редактирование: сервер передаёт выбранную версию и промпт агенту Codex CLI (codex exec). Агент редактирует изображение и сохраняет результат. Обработка обычно занимает 13 минуты.
  • Увеличение разрешения: 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 и запускается вручную: вкладка ActionsRun 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

Требования для работы кнопки:

  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 ещё не зарегистрирован:

# токен регистрации: Администрирование → 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
  1. После регистрации нажмите «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).

Требования:

  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-editorEditEnvironment variablesAPP_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)

Выводы:

  • Выбор модели почти не влияет на время (6082 s): узкое место — вызов image-модели при редактировании, а не агентная модель.
  • Единственный заметный выигрыш — CODEX_REASONING_EFFORT=low (~17%). Кстати, у gpt-5.6-sol low — это значение по умолчанию; в ~/.codex/config.toml оно переопределено на high, поэтому редактирование такое долгое.
  • Для радикального ускорения нужно уменьшать разрешение исходника для шага редактирования (медленнее при 1200×800, чем при 512×512) — но тогда результат будет меньшего размера.
Description
No description provided
Readme 476 KiB
Languages
JavaScript 85.9%
CSS 9.3%
HTML 4.2%
Dockerfile 0.6%