v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Some checks failed
test / test (push) Failing after 24s
Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (docs/architecture/target-design.md): - src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images) - Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue) - Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов - Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history - Даунскейл sharp до 1024px перед Codex + масштабирование области референса - SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/) - Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data - Документация: README, docs/architecture/
This commit is contained in:
360
README.md
360
README.md
@@ -1,4 +1,4 @@
|
||||
# Кадр — локальный AI-фоторедактор
|
||||
# Кадр — локальный AI-фоторедактор (v2)
|
||||
|
||||
Веб-приложение редактирует фотографии по текстовому промпту (через локальный
|
||||
Codex CLI) и увеличивает разрешение через домашний сервер ComfyUI.
|
||||
@@ -7,292 +7,130 @@ Codex CLI) и увеличивает разрешение через домаш
|
||||
преобразуются на сервере в JPEG с помощью `heic-convert` до передачи в ComfyUI
|
||||
или Codex.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура v2
|
||||
|
||||
В версии v2 сервис переработан в **модульный монолит** (`src/`):
|
||||
|
||||
1. **Устойчивость к перезапуску**:
|
||||
- Персистентный append-only JSONL-журнал заданий (`data/jobs.jsonl`). При рестарте сервера незавершённые задания Codex получают статус `interrupted` с возможностью повтора в один клик, а задания ComfyUI автоматически возвращаются в очередь.
|
||||
- Персистентное хранилище сессий (`data/sessions.json`, хранятся sha256-хеши токенов). Владелец остаётся авторизован после обновления или перезапуска контейнера.
|
||||
- Атомарная запись манифеста истории (`history.json.tmp` + `fsync` + `rename`) защищает от повреждения данных при внезапном выключении.
|
||||
2. **Очередь заданий (FIFO) и группы конкурентности**:
|
||||
- Вместо блокировки single-flight (429) запросы на редактирование и апскейл ставятся в очередь.
|
||||
- Лимит параллельности: Codex (edit, suggest) = 1 поток; ComfyUI (upscale) = 2 потока.
|
||||
- Лимит очереди: до 5 заданий на группу (защита от накликивания).
|
||||
3. **Безопасность**:
|
||||
- Пароль защищён scrypt-хешированием (`crypto.scrypt`) с постоянным временем сверки (`timingSafeEqual`).
|
||||
- Ограничение попыток входа (Rate Limiting): 5 попыток за 15 минут на IP.
|
||||
- Проверка Origin на state-changing запросах (CSRF).
|
||||
- Проверка сигнатур файлов (magic bytes) для защиты от поддельных MIME-типов.
|
||||
4. **Ускорение редактирования (Downscale)**:
|
||||
- Автоматический даунскейл `sharp` до `EDIT_MAX_DIMENSION` (по умолчанию 1024 px) перед передачей в Codex с пропорциональным масштабированием области референса (`scaleRegion`). Сокращает время обработки больших фото в 1.5–2 раза.
|
||||
5. **Мгновенные обновления статуса (SSE)**:
|
||||
- Server-Sent Events (`GET /api/events`) передают смену статуса очереди и прогресс мгновенно.
|
||||
- Автоматический фолбэк на опрос (`/api/jobs`) при сбоях сети или неподдерживаемых прокси.
|
||||
6. **Клиентская часть на Vanilla ES-модулях**:
|
||||
- Модули в `public/js/` загружаются напрямую браузером (`<script type="module">`) без этапа сборки.
|
||||
|
||||
---
|
||||
|
||||
## Как это работает
|
||||
|
||||
- **История версий**: каждая загрузка и каждый результат (редактирование или
|
||||
апскейл) сохраняется в папке `history/` как отдельная версия. Можно вернуться
|
||||
к любой версии кликом по ней в ленте «История версий» и продолжить правки
|
||||
именно с неё — например, отредактировать фото, а затем увеличить разрешение
|
||||
уже отредактированного варианта. История переживает перезапуск сервера и
|
||||
обновление страницы; хранится последние 50 версий.
|
||||
- **Редактирование**: сервер передаёт выбранную версию и промпт агенту Codex
|
||||
CLI (`codex exec`). Агент редактирует изображение и сохраняет результат.
|
||||
Обработка обычно занимает 1–3 минуты.
|
||||
- **Увеличение разрешения**: 4× upscale через ComfyUI моделью
|
||||
`RealESRGAN_x4plus.safetensors` — применяется к выбранной (в том числе
|
||||
отредактированной) версии.
|
||||
- **История версий**: каждая загрузка и результат (редактирование или апскейл) сохраняются в `data/history/` как отдельная версия. Доступно интерактивное дерево версий с ветвлением от исходника, каскадным удалением и свёрткой групп. Хранятся последние 50 версий (настраивается через `HISTORY_LIMIT`).
|
||||
- **Редактирование**: сервер передаёт выбранную версию и промпт агенту Codex CLI (`codex exec`). Агент редактирует изображение и сохраняет результат в JPEG.
|
||||
- **Увеличение разрешения**: 4× upscale через ComfyUI моделью `RealESRGAN_x4plus.safetensors` (или `4x_UltraSharp`).
|
||||
- **Редактирование с референсом и областью**: можно задать референс — файл или версию из истории. На превью референса выделяется прямоугольник области; координаты передаются в промпт Codex.
|
||||
- **Подсказки промптов**: мгновенные варианты из недавней истории и генерация новых вариантов через Codex.
|
||||
|
||||
Редактирование и апскейл выполняются в фоне: `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` (тот же механизм опроса статуса, что
|
||||
и у редактирования).
|
||||
## Переменные окружения
|
||||
|
||||
## Требования
|
||||
| Переменная | Тип | Дефолт | Описание |
|
||||
|---|---|---|---|
|
||||
| `PORT` | number | `3000` | Порт HTTP-сервера (кандидаты по умолчанию: 3000, 8080, 8090) |
|
||||
| `COMFY_URL` | url | `http://192.168.31.240:8188` | Адрес домашнего сервера ComfyUI |
|
||||
| `APP_USER` | string | `admin` | Логин владельца |
|
||||
| `APP_PASSWORD` | string | *автогенерация* | Пароль владельца (если не задан — печатается в лог при старте) |
|
||||
| `LOG_LEVEL` | string | `info` | Уровень логов (`debug`, `info`, `warn`, `error`) |
|
||||
| `DATA_DIR` | path | `./data` | Каталог данных (сессии, журнал заданий, история) |
|
||||
| `HISTORY_LIMIT` | number | `50` | Максимальное количество версий в истории |
|
||||
| `MAX_UPLOAD_BYTES` | number | `26214400` (25 МБ) | Максимальный размер загружаемого файла |
|
||||
| `EDIT_MAX_DIMENSION` | number | `1024` | Максимальный размер стороны при даунскейле перед Codex (0 — отключить) |
|
||||
| `UPSCALE_MAX_DIMENSION`| number | `2048` | Предел входного разрешения перед 4× апскейлом |
|
||||
| `CODEX_CLI_PATH` | string | `codex` / авто-поиск | Путь к бинарнику Codex CLI |
|
||||
| `CODEX_EDIT_TIMEOUT_MS`| number | `420000` (7 мин) | Таймаут выполнения операции в Codex |
|
||||
| `JOB_TIMEOUT_MS` | number | `300000` (5 мин) | Таймаут задачи в ComfyUI |
|
||||
| `REQUEST_TIMEOUT_MS` | number | `15000` (15 с) | Таймаут сетевых HTTP-запросов |
|
||||
| `SESSION_TTL_MS` | number | `604800000` (7 дней) | Срок жизни cookie-сессии (скользящий) |
|
||||
| `SESSION_PERSIST` | bool | `true` | Сохранение сессий между перезапусками сервера |
|
||||
| `CODEX_CONCURRENCY` | number | `1` | Число параллельных потоков Codex |
|
||||
| `UPSCALE_CONCURRENCY` | number | `2` | Число параллельных потоков ComfyUI |
|
||||
| `MAX_QUEUED_JOBS` | number | `5` | Максимальный размер очереди на группу |
|
||||
| `SSE_ENABLED` | bool | `true` | Включение Server-Sent Events |
|
||||
| `TRUST_PROXY` | bool | `false` | Доверие заголовкам прокси (`X-Forwarded-For` для rate-limiting) |
|
||||
|
||||
- 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 test
|
||||
|
||||
# Запуск приложения
|
||||
npm start
|
||||
|
||||
# Запуск в режиме разработки с автоперезагрузкой
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
После запуска редактор закрыт cookie-сессией. По умолчанию логин — `admin`,
|
||||
а пароль генерируется при старте сервера и печатается в консоль (баннер
|
||||
`APP_PASSWORD не задан. Сгенерирован случайный пароль`). Чтобы зафиксировать
|
||||
учётные данные, задайте переменные окружения `APP_USER` / `APP_PASSWORD`.
|
||||
Подробнее — в разделе «Авторизация».
|
||||
При первом запуске без `APP_PASSWORD` в консоль выводится баннер со случайным сгенерированным паролем.
|
||||
|
||||
## Авторизация
|
||||
---
|
||||
|
||||
Редактор «Кадр» закрыт cookie-сессией: все `/api/*` эндпоинты, кроме
|
||||
`/api/health`, `/api/login`, `/api/me`, `/api/logout`, требуют валидную
|
||||
сессию, иначе возвращают `401 {"ok":false,"error":"Требуется авторизация."}`.
|
||||
На главной странице поверх редактора показывается форма входа, которая
|
||||
блокирует работу, пока владелец не войдёт. Статические файлы и `/api/health`
|
||||
остаются публичными.
|
||||
## Авторизация и безопасность
|
||||
|
||||
Учётные данные задаются переменными окружения:
|
||||
Редактор защищён cookie-сессией `kadr_session` (`HttpOnly`, `SameSite=Lax`):
|
||||
- Публичные маршруты: `GET /api/health`, `POST /api/login`, `GET /api/me`, `POST /api/logout`.
|
||||
- Все остальные маршруты `/api/*` требуют авторизации (`401 Unauthorized`).
|
||||
- Защита от перебора: после 5 неудачных попыток входа IP блокируется на 15 минут.
|
||||
- Пароль проверяется функцией scrypt (`crypto.scrypt`) с защитой от атак по времени (`timingSafeEqual`).
|
||||
|
||||
- `APP_USER` — логин владельца (по умолчанию `admin`).
|
||||
- `APP_PASSWORD` — пароль. Если не задан или пуст, сервер генерирует
|
||||
случайный 16-символьный пароль при старте и печатает его в консоль/лог
|
||||
контейнера (баннер `APP_PASSWORD не задан. Сгенерированный пароль`).
|
||||
---
|
||||
|
||||
Сессия — cookie `kadr_session`:
|
||||
## Логирование и диагностика
|
||||
|
||||
- срок жизни 7 суток;
|
||||
- скользящая (sliding): продлевается при каждой активности;
|
||||
- `HttpOnly`, `SameSite=Lax`, без `Secure` (приложение работает по HTTP
|
||||
в локальной сети);
|
||||
- сессии хранятся в памяти сервера — перезапуск сбрасывает все входы.
|
||||
Все логи формируются в структурированном формате JSON-lines с обязательным полем `ts`, `level`, `msg` и `requestId`:
|
||||
|
||||
Публичные эндпоинты: `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":"Требуется авторизация."}
|
||||
```json
|
||||
{"ts":"2026-08-27T12:00:00.000Z","level":"info","msg":"HTTP запрос завершён","requestId":"req_a1b2c3d4","method":"GET","url":"/api/history","status":200,"durationMs":3}
|
||||
```
|
||||
|
||||
## Docker
|
||||
Пароли, сессионные токены и персональные секреты автоматически маскируются (`[REDACTED]`).
|
||||
|
||||
Образ собирается на базе `node:22-bookworm-slim` (Debian, glibc) — Codex CLI
|
||||
распространяется как бинарник под glibc и не работает на Alpine/musl.
|
||||
---
|
||||
|
||||
## Docker и развёртывание
|
||||
|
||||
### Локальный Docker
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Что важно знать:
|
||||
### Развёртывание в Portainer через Gitea Actions
|
||||
|
||||
- **Авторизация 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) — но тогда
|
||||
результат будет меньшего размера.
|
||||
1. Workflow `.gitea/workflows/test.yml` автоматически запускает автотесты при push в `main`.
|
||||
2. Workflow `.gitea/workflows/build.yml` собирает образ на базе `node:22-bookworm-slim` и пушит в Gitea Registry.
|
||||
3. Workflow `.gitea/workflows/deploy.yml` обновляет стек в Portainer с томами:
|
||||
- `kadr-data:/app/data` — данные v2 (сессии, журнал, история);
|
||||
- `kadr-history:/app/history` — старый том истории (автоматически мигрируется при первом старте v2);
|
||||
- `/root/.codex:/root/.codex` — конфигурация и авторизация Codex.
|
||||
|
||||
Reference in New Issue
Block a user