Files

566 lines
33 KiB
Markdown
Raw Permalink 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.
# comfy-adapter
**OpenAI-совместимый HTTP-адаптер для [ComfyUI](https://github.com/Comfy-Org/ComfyUI).**
Адаптер принимает стандартные запросы OpenAI Images API (`/v1/images/generations`,
`/v1/images/edits`), подставляет параметры в заранее подготовленные workflow-шаблоны,
выполняет их на сервере ComfyUI и возвращает готовое изображение в поле `b64_json`.
Клиенту не нужно ничего знать о нодах, графах и формате workflow — ComfyUI становится
обычным image-бэкендом с OpenAI-контрактом.
```
+----------------+ OpenAI Images API +----------------+ ComfyUI HTTP API +----------------+
| | --------------------> | | ------------------> | |
| Любой клиент | /v1/images/* | comfy-adapter | /prompt | ComfyUI |
| (OpenAI SDK, | Bearer <ключ> | (FastAPI) | /upload/image | (GPU-сервер) |
| LangChain, | JSON / multipart | | /history/{id} | |
| curl, ...) | <-------------------- | | <------------------ | |
+----------------+ b64_json (PNG) +----------------+ /view +----------------+
```
**Модели**
В API различаются два понятия «модель». **API-модель** — внешний алиас из реестра
`models.json`, который лишь выбирает, какой workflow выполнить (см. «Реестр моделей»).
**Модель ComfyUI** (диффузионный чекпойнт, LoRA, CLIP, VAE) неизменяема и зашита внутрь
самого workflow-графа — она не подставляется из запроса. Каждая API-модель жёстко привязана
к своему workflow-файлу с уже подобранной цепочкой моделей.
| id модели в API | Назначение | Диффузионная модель (внутри workflow) | Доп. |
|---|---|---|---|
| `z-image-turbo` | генерация по текстовому промпту | `z_image_turbo_bf16.safetensors` | CLIP `qwen_3_4b` (lumina2), VAE `ae` |
| `qwen-image-edit` | редактирование входного изображения | `qwen_image_edit_2509_fp8_e4m3fn.safetensors` | LoRA `Qwen-Image-Edit-2509-Lightning-4steps-V1.0-bf16` (4 шага), CLIP `qwen_2.5_vl_7b_fp8_scaled` (qwen_image), VAE `qwen_image_vae` |
Проверено на ComfyUI **0.37.0** (Tesla V100 16GB, CUDA). Используются только штатные
ноды и официальные HTTP-эндпоинты ComfyUI — кастомные ноды не требуются
(`TextEncodeQwenImageEditPlus` входит в ядро, `comfy_extras.nodes_qwen`).
---
## Возможности
- **Два эндпоинта** в контракте OpenAI: генерация и редактирование изображений.
- **`GET /v1/models`** — список доступных «моделей» (для автодополнения в клиентах).
- **Воспроизводимость**: опциональный `seed`; один и тот же `seed` + промпт дают идентичный результат.
- **Толерантный парсинг OpenAI**: неприменимые поля (`quality`, `style`, `user`) не ломают запрос.
- **Workflow как данные**: реестр моделей `models.json` и workflow-файлы с плейсхолдерами вынесены из кода.
- **Защита от деградации графа**: битые ссылки в шаблоне обнаруживаются локально и на сервере, адаптер не стартует с нерабочим workflow.
- **Статический API-ключ** на всех эндпоинтах, кроме `GET /health`.
- **Ошибки в формате OpenAI** (`error.message/type/param/code`) — клиенты корректно их разбирают.
- **Ответ всегда base64**: адаптеру и клиенту не нужен сетевой доступ к ComfyUI.
---
## Быстрый старт
### Локально
```bash
cd comfy-adapter
python -m venv .venv
# Linux/macOS: .venv/bin/pip install -e . Windows: .venv\Scripts\pip install -e .
cp .env.example .env # заполните COMFY_BASE_URL и ADAPTER_API_KEY
uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-level info
```
### Docker
```bash
docker build -t comfy-adapter .
docker run -p 8000:8000 --env-file .env comfy-adapter
```
### Проверка
```bash
curl -s http://localhost:8000/health
# {"status":"ok"}
```
Конфигурация по умолчанию указывает на `http://192.168.0.3:8188` — поменяйте
`COMFY_BASE_URL`, если ваш сервер другой. ComfyUI должен быть запущен с `--listen`,
иначе адаптер до него не достучится.
---
## Конфигурация
Все настройки — переменные окружения (или `.env` в рабочей директории).
| Переменная | По умолчанию | Назначение |
|---|---|---|
| `COMFY_BASE_URL` | `http://192.168.0.3:8188` | Базовый адрес сервера ComfyUI |
| `ADAPTER_API_KEY` | пусто (**обязательно**) | Ожидаемый Bearer-ключ клиентов. Пустое значение = все запросы получают 401 (fail-closed) |
| `EXECUTION_TIMEOUT` | `300` | Секунд ожидания результата от ComfyUI; превышение -> 502 `comfyui_timeout` |
| `POLL_INTERVAL` | `1.5` | Интервал опроса `/history`, сек |
| `MODELS_FILE` | `<репо>/models.json` | Реестр моделей: JSON `id -> {workflow, owned_by}` (обязателен, см. «Реестр моделей») |
| `VALIDATE_TEMPLATES_ON_SERVER` | `true` | Прогонять собранные графы через `POST /prompt?front=false` при старте |
Логирование стартовых проверок видно с `uvicorn --log-level info`:
```
INFO:comfy-adapter:workflow templates validated on ComfyUI server
INFO:comfy-adapter:comfy-adapter ready (server=http://192.168.0.3:8188)
```
---
## API
### Аутентификация
Все эндпоинты, кроме `GET /health`, требуют заголовок:
```
Authorization: Bearer <ADAPTER_API_KEY>
```
Отсутствие или несовпадение ключа -> `401` (`code: "invalid_api_key"`). Сравнение
постоянного времени (`secrets.compare_digest`).
### `POST /v1/images/generations`
Только `application/json`.
| Параметр | Тип | По умолчанию | Обязателен | Описание |
|---|---|---|---|---|
| `prompt` | string | — | **да** | Текстовое описание. Пустая строка -> 400 |
| `model` | string \| null | `null` | нет | Id модели из реестра; пусто -> первая совместимая с генерацией (`z-image-turbo`). Неизвестная/несовместимая -> 400 |
| `n` | int \| null | `1` | нет | Поддерживается только `1`, иначе 400 |
| `size` | string | `1024x1024` | нет | Строго `WxH`, оба значения > 0. Попала в latent-ноду, то есть реально задаёт разрешение |
| `seed` | int \| null | `null` | нет | `0..2^63-1`. Без него — криптографически случайный (`secrets.randbelow`) |
| `response_format` | string \| null | `null` | нет | Принимается; всегда возвращается `b64_json` |
**Ответ 200**
```json
{
"created": 1791145663,
"data": [{ "b64_json": "<base64 PNG>" }]
}
```
### `POST /v1/images/edits`
Принимаются оба транспорта; набор полей одинаковый.
**multipart/form-data** (как в OpenAI):
| Поле | Тип | Обязателен | Описание |
|---|---|---|---|
| `image` | файл | **да** | PNG или JPEG (проверка по сигнатуре байтов) |
| `prompt` | string | **да** | Инструкция по редактированию |
| `model` | string | нет | Id из реестра, совместимый с edits; пусто -> первая такая (`qwen-image-edit`). Иначе 400 |
| `seed` | int | нет | В форме передаётся строкой, парсится в int |
**application/json:**
| Поле | Тип | Обязателен | Описание |
|---|---|---|---|
| `image` | string (base64) | **да** | Допускается data-URL-префикс `data:image/png;base64,...` |
| `prompt` | string | **да** | |
| `model` | string | нет | Id из реестра, совместимый с edits; пусто -> первая такая (`qwen-image-edit`). Иначе 400 |
| `seed` | int | нет | |
| `size` | string | нет | **Принимается, но не влияет на результат** — см. «Ограничения» |
**Ответ** — тот же, что у генерации.
### `GET /v1/models`
Требует ключ. Ответ:
```json
{
"object": "list",
"data": [
{ "id": "z-image-turbo", "object": "model", "owned_by": "comfy-adapter" },
{ "id": "qwen-image-edit", "object": "model", "owned_by": "comfy-adapter" }
]
}
```
### `GET /health`
Без ключа. `{"status":"ok"}` — для health-чеков оркестраторов и reverse proxy.
### Ошибки
Канонический формат OpenAI:
```json
{ "error": { "message": "...", "type": "...", "param": null, "code": "..." } }
```
| HTTP | `type` | `code` | Причина |
|---|---|---|---|
| 400 | `invalid_request_error` | `null` | Пустой `prompt`, `n>1`, неверный формат `size`, неизвестная `model`, битый JSON/base64, отсутствующий или недекодируемый `image` |
| 401 | `invalid_request_error` | `invalid_api_key` | Ключ отсутствует или не совпадает (в т.ч. при незаполненном `ADAPTER_API_KEY`) |
| 502 | `server_error` | `comfyui_validation_failed` | ComfyUI отверг граф; в `message` — разбор `node_errors` с id узлов |
| 502 | `server_error` | `comfyui_execution_failed` | Задание упало во время исполнения (`status_str: error`) |
| 502 | `server_error` | `comfyui_timeout` | Результат не появился за `EXECUTION_TIMEOUT` |
| 502 | `server_error` | `comfyui_no_output` | В истории нет изображений типа `output` |
| 502 | `server_error` | `comfyui_error` | ComfyUI недоступен или вернул неожиданный статус |
---
## Примеры
### cURL
```bash
# Генерация
curl -s http://localhost:8000/v1/images/generations \
-H "Authorization: Bearer $ADAPTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt":"a red apple on a wooden table next to steaming coffee","size":"1024x1024","seed":421337}' \
| jq -r .data[0].b64_json | base64 -d > out.png
# Редактирование (multipart)
curl -s http://localhost:8000/v1/images/edits \
-H "Authorization: Bearer $ADAPTER_API_KEY" \
-F image=@out.png \
-F prompt="change the cup to deep blue ceramic, background a dark kitchen at night, keep the apple the same" \
| jq -r .data[0].b64_json | base64 -d > edited.png
# Список моделей
curl -s http://localhost:8000/v1/models -H "Authorization: Bearer $ADAPTER_API_KEY" | jq
```
### Официальный Python SDK
```python
import base64
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="sk-1234")
# Генерация. seed нет в подписи SDK -> передаём через extra_body
resp = client.images.generate(
model="z-image-turbo",
prompt="a red apple on a wooden table next to steaming coffee",
size="1024x1024",
n=1,
extra_body={"seed": 421337},
)
open("out.png", "wb").write(base64.b64decode(resp.data[0].b64_json))
# Редактирование того же изображения
with open("out.png", "rb") as f:
edited = client.images.edit(
model="qwen-image-edit",
image=("out.png", f.read(), "image/png"),
prompt="change the cup to deep blue ceramic, keep the apple the same",
)
open("edited.png", "wb").write(base64.b64decode(edited.data[0].b64_json))
```
Проверено с `openai` 3.24.0 — оба вызова проходят без кастомного транспорта.
### JavaScript
```js
import OpenAI from "openai";
import fs from "fs";
const client = new OpenAI({ baseURL: "http://localhost:8000/v1", apiKey: "sk-1234" });
const r = await client.images.generate({
model: "z-image-turbo",
prompt: "a red apple on a wooden table",
size: "1024x1024",
n: 1,
seed: 421337, // поддерживается адаптером как обычное поле JSON-тела
});
fs.writeFileSync("out.png", Buffer.from(r.data[0].b64_json, "base64"));
```
---
## Как это работает
Один и тот же цикл для обоих эндпоинтов:
```
request
|
v
[1] auth + валидация тела --> 400 / 401
|
v
[2] deep-copy workflow выбранной модели, замещение плейсхолдеров {prompt}/{seed}/{width}/{height}/{image} значениями запроса
|
v
[3] (только edits) POST /upload/image -> имя файла в input-каталоге ComfyUI
|
v
[4] POST /prompt (client_id = uuid4 на запрос) --> prompt_id (400 => comfyui_validation_failed)
|
v
[5] poll GET /history/{prompt_id} каждые POLL_INTERVAL, до EXECUTION_TIMEOUT
| (status_str=error => comfyui_execution_failed)
v
[6] GET /view?filename=...&type=output -> байты PNG
|
v
[7] base64 -> data[0].b64_json
```
WebSocket `/ws` намеренно не используется: ComfyUI гарантированно пишет запись в
историю после завершения задания, поэтому опрос проще, не держит долгоживущее
соединение и устойчив к разрывам. Транспорт изолирован в `app/comfy_client.py` —
переход на websocket не затронул бы остальной код.
### Реестр моделей (models.json)
Список API-моделей задаётся **декларативно**, в файле `models.json` (путь — env
`MODELS_FILE`, по умолчанию `<репо>/models.json`). Каждая запись связывает id модели с её
workflow-файлом:
```json
{
"z-image-turbo": { "workflow": "workflows/z_image_turbo.json", "owned_by": "comfy-adapter" },
"qwen-image-edit": { "workflow": "workflows/qwen_image_edit.json", "owned_by": "comfy-adapter" }
}
```
- **Добавить модель = добавить запись + файл workflow. Код править не нужно.**
- `workflow` — путь к JSON (относительный разрешается относительно папки `models.json`).
- `owned_by` необязателен (по умолчанию `comfy-adapter`); используется в `GET /v1/models`.
- **Порядок записей значим**: он задаёт приоритет выбора при `model` не задан.
- Файл обязателен: отсутствующий или битый `models.json` -> явная ошибка конфигурации на
старте (fail-fast), сервис не поднимется молча.
**Выбор модели.** Поле `model` выбирает запись реестра. Если `model` не задан или `null` —
берётся **первая совместимая с эндпоинтом** модель. Неизвестный id или несовместимый с
эндпоинтом -> `400`. Совместимость вычисляется из набора токенов (см. ниже), отдельного
поля «тип модели» в реестре нет.
**Совместимость модели с эндпоинтом** — вложенность множеств токенов, наборы принадлежат
эндпоинту:
```
модель допустима для эндпоинта <=> required(эндп) ⊆ токены(граф) ⊆ supplied(эндп)
эндпоинт supplied (что может подставить) required (обязательно в графе)
generations prompt, negative, seed, width, height prompt
edits prompt, negative, seed, image prompt, image
```
Отсюда: генерация не примет модель с `{image}` (не может подставить), а редактирование не
примет модель без `{image}` (картинка не должна остаться незамещённой).
### Плейсхолдеры в workflow
Точка внедрения значений находится **внутри workflow-графа** — в значениях полей узлов
пишутся токены вида `{имя}`. Никакой внешней карты узел↔поле нет; соответствие задаёт сам
граф. Фиксированный словарь токенов принадлежит адаптеру:
| Токен | Тип | Кто подставляет |
|---|---|---|
| `{prompt}` | строка | оба эндпоинта |
| `{negative}` | строка | всегда `""` (запрос негатив не принимает) |
| `{seed}` | число | оба эндпоинта |
| `{width}` / `{height}` | число | только генерация (из `size`) |
| `{image}` | строка | только edits (имя загруженного файла) |
Пример узла KSampler с плейсхолдерами:
```json
"83:3": { "class_type": "KSampler", "inputs": { "seed": "{seed}", "...": 0, "model": ["83:28", 0] } }
```
Правила подстановки:
- значение узла **равно ровно** `{seed}` -> подставляется **число** (типы сохраняются;
иначе `front=false` не примет строку в int-виджете); токен внутри текста -> строковая
вставка без искажения окружения;
- нераспознанный токен (вне словаря) -> workflow отклоняется на старте;
- незамещённый токен (эндпоинт не поставил значение) -> запрос отклоняется, ComfyUI не
получает задание.
### Проверка workflow
Двухуровневая, оба уровня на старте, для **каждой** модели реестра:
1. **Локально** (`app/templates.py`): обход `inputs` в поиске ссылок `[node_id, slot]` на
несуществующие узлы и проверка токенов (нет ли вне словаря). Найденное -> исключение со
списком `node X: input 'Y' links to missing node 'Z'`, адаптер не стартует.
2. **На сервере** (`VALIDATE_TEMPLATES_ON_SERVER=true`): граф каждой модели собирается
типизированными значениями-заглушками (dry-inject: `seed=1`, `width=height=1024`,
`prompt="x"`, `negative=""`, `image="dummy.png"`) и отправляется на
`POST /prompt?front=false` — ComfyUI валидирует граф, **не ставя задание в очередь** и
не тратя GPU. Заодно прогоняется сам инжектор. Ошибка логируется как диагностика старта
и не блокирует запуск (согласно прежнему поведению).
---
## Workflow-файлы и их починка
Оба workflow-файла были выгружены из OpenWebUI (там прямая интеграция с ComfyUI). В
**исходном виде они не проходили валидацию ComfyUI** — в цепочках моделей
отсутствовали узлы, на которые шли ссылки:
```
workflows/z_image_turbo.json workflows/qwen_image_edit.json
83:28 UNETLoader 115:37 UNETLoader
| на него никто не ссылается | на него никто не ссылается
v v
83:3 KSampler ---> 83:43 (НЕТ В ГРАФЕ) 115:66 ModelSamplingAuraFlow ---> 115:117 (НЕТ В ГРАФЕ)
83:27 CLIPTextEncode -> 83:43 (НЕТ) 115:89 LoraLoaderModelOnly (Lightning 4-step) - висит без потребителей
```
Ошибка сервера: `prompt_outputs_failed_validation`, `KeyError: '83:43'` / `'115:117'`.
Исправления, применённые в репозиторных файлах (минимальные, по одной ссылке на workflow;
в те же файлы добавлены плейсхолдеры `{prompt}`/`{seed}`/`{width}`/`{height}`/`{image}`/`{negative}`):
| Workflow | Было | Стало |
|---|---|---|
| `workflows/z_image_turbo.json` | `83:3.inputs.model = ["83:43", 0]`, `83:27.inputs.clip = ["83:43", 1]` | `["83:28", 0]` (UNETLoader), `["83:30", 0]` (CLIPLoader) |
| `workflows/qwen_image_edit.json` | `115:66.inputs.model = ["115:117", 0]` | `["115:89", 0]` — LoRA-нода `115:89` подключена в цепочку (`115:37 -> 115:89 -> 115:66 -> 115:75 -> KSampler`) |
Подключение LoRA во втором случае содержательно верно: в настройках OpenWebUI в поле
«Модель» редактирования указан именно Lightning-LoRA на 4 шага, а `steps: 4` в
KSampler без него не имеет смысла.
**Эффект:** оба графа проходят `front=false` и реально выполняют полный цикл —
проверено end-to-end на ComfyUI 0.37.0 (V100).
При смене workflow в OpenWebUI следует перезалить JSON сюда и сверить эти две
цепочки; локальная проверка на старте поймает проблему автоматически.
### Свойства workflow, которые стоит знать
- **Размер выхода в edit ≠ `size`.** В графе за разрешение отвечает
`ImageScaleToTotalPixels` (`megapixels: 1`), а latent из `EmptySD3LatentImage`
перебивается `VAEEncode` входного изображения. Итог — ~1 Мп с аспектом входной
картинки. Совместимо с OpenAI, который в edits размер тоже фактически игнорирует.
- **Negative prompt в create не используется.** В графе стоит `ConditioningZeroOut`,
поэтому Z-Image Turbo генерирует без негатива — это свойство модели (turbo/cfg=1),
а не ограничение адаптера.
- **Параметры KSampler (`steps`, `cfg`, `sampler_name`, `scheduler`) и пути к моделям**
закреплены в шаблонах и из запроса не меняются. Правятся редактированием JSON.
---
## Структура проекта
```
comfy-adapter/
├── app/
│ ├── main.py FastAPI: эндпоинты, цикл execute, разбор multipart/JSON
│ ├── config.py настройки из env + загрузка реестра моделей из models.json
│ ├── comfy_client.py async-клиент ComfyUI: /prompt, /upload/image, /history, /view
│ ├── injector.py deep-copy workflow + замещение плейсхолдеров, парсинг WxH, seed
│ ├── templates.py загрузка реестра, проверка ссылок и токенов, выбор модели
│ ├── schemas.py pydantic-модели OpenAI-контракта
│ ├── errors.py иерархия исключений + обработчики в формат OpenAI
│ └── auth.py проверка статического Bearer-ключа
├── tests/
│ ├── conftest.py фикстура: приложение с lifespan и ComfyUI, указывающим на мок
│ ├── test_generations.py генерация: успех, инъекция значений, seed, все классы 400/502
│ ├── test_edits.py edits: multipart и base64, игнорирование size, ошибки входа
│ ├── test_comfy_client.py каждый метод клиента на respx-моках (успех/HTTP-ошибка/таймаут)
│ ├── test_injector.py плейсхолдеры, deep-copy без мутации, seed, парсинг size
│ ├── test_templates.py битые ссылки, токены, совместимость и выбор модели, ошибки реестра
│ ├── test_config.py дефолты, переопределение из env, загрузка и fail-fast реестра
│ ├── test_auth.py ключ: отсутствует / неверный / верный; health без ключа
│ ├── test_errors.py канонический вид error-body, соответствие статусов
│ ├── test_models.py формат /v1/models
│ ├── test_registry_integration.py новая модель только правкой models.json (без кода)
│ └── test_live.py живые прогоны против реального ComfyUI (маркер live)
├── models.json реестр API-моделей: id -> {workflow, owned_by}
├── workflows/ workflow-файлы с плейсхолдерами (z_image_turbo.json, qwen_image_edit.json)
├── Dockerfile python:3.12-slim + uvicorn
├── pyproject.toml
└── .env.example
```
**Технологии:** Python 3.12+, FastAPI, uvicorn, httpx (async), pydantic v2 +
pydantic-settings. Тесты: pytest, pytest-asyncio, respx.
---
## Тесты
```bash
pip install -e ".[dev]"
pytest # юнит- и интеграционные тесты на моках ComfyUI (respx) — сеть не нужна
```
Живые прогоны против реального ComfyUI (тратят GPU-время, реальные генерации):
```bash
uvicorn app.main:app --port 8000 &
export ADAPTER_LIVE=1 ADAPTER_URL=http://127.0.0.1:8000 ADAPTER_KEY=<ваш ключ>
pytest -m live
```
Проверяют: генерацию 1024x1024 с декодированием и верным размером, детерминизм по
seed (повтор даёт идентичный base64), редактирование с выходом ~1 Мп и сохранением
аспекта входа.
---
## Ограничения
- **`n > 1` не поддерживается** -> 400. Нужен — правьте `batch_size` в workflow или
запускайте несколько запросов.
- **`size` в `/v1/images/edits` не влияет на выход** (см. «Свойства workflow»): edit-граф не
содержит `{width}`/`{height}`, эндпоинт edits их не поставляет.
- **`negative_prompt` из запроса не принимается.** Поля в схеме запроса нет — токен
`{negative}`, если он есть в workflow, всегда резолвится в пустую строку `""`.
- **`model` не задан -> берётся первая совместимая с эндпоинтом модель** реестра (порядок
записей в `models.json` задаёт приоритет). Явный `default`-флаг пока не реализован.
- **Размер в генерации не квантуется.** Любое корректное `WxH` уходит в latent
напрямую; нестандартные значения могут дать артефакты или ошибку уже на стороне
ComfyUI (прилетит как 502, а не 400).
- **`response_format: "url"` не реализован** — всегда `b64_json`.
- **Синхронная модель.** HTTP-соединение держится до конца генерации; отдельной
очереди заданий нет, конкурентные запросы сериализуются очередью ComfyUI.
- **Один сервер ComfyUI** из `COMFY_BASE_URL`, без пула и балансировки.
- **Загружаемые изображения** (edits) остаются в input-каталоге ComfyUI. Имена
уникальны (`adapter_<uuid>_<rand>.png`), коллизий нет, автоматической очистки нет.
- **Неизвестные поля запроса игнорируются** (стандартное поведение pydantic), а не
отклоняются.
## Дорожная карта (осознанно отложено)
`negative_prompt` в запросе -> `quantize` размера под кратные значения ->
несколько ComfyUI-серверов и очереди -> `response_format: "url"` для публикации за
пределами LAN -> websocket вместо опроса, если понадобится меньшая задержка.
---
## Эксплуатация
- **Reverse proxy.** Ответ синхронный и может занимать десятки секунд — поднимите
`proxy_read_timeout` (nginx) или эквивалент выше, чем `EXECUTION_TIMEOUT`, иначе
прокси оборвёт соединение раньше адаптера.
- **VRAM.** На 16GB V100 модели догружаются и выгружаются по очереди; при
чередовании генерации и редактирования первые кадры медленнее из-за загрузки
весов. Это поведение ComfyUI, адаптер на него не влияет.
- **Безопасность.** Ключ задаётся только через окружение, в репозиторий не попадает
(`.env` в `.gitignore`). При пустом `ADAPTER_API_KEY` сервис закрыт по умолчанию.
- **Развёртывание рядом с OpenWebUI.** Адаптер ничего не мигрирует: нативная
интеграция OpenWebUI с ComfyUI продолжает работать напрямую, адаптер —
независимый параллельный путь к тем же workflow.
- **Мониторинг.** `GET /health` без ключа; стартовые строки
`workflow templates validated...` / `comfy-adapter ready` подтверждают
работоспособность графов.
---
## Проверенное поведение
Не только тесты — контракт прогонялся живьём через официальный OpenAI SDK:
| Шаг | Вызов | Результат |
|---|---|---|
| Генерация | `client.images.generate(model="z-image-turbo", seed=421337, size="1024x1024")` | 200, PNG 1024x1024 (~1.0 МБ), кадр точно по промпту (яблоко, дымящий кофе, деревянный стол, мягкий свет) |
| Редактирование | `client.images.edit(model="qwen-image-edit", image=out.png, prompt=...)` | 200, PNG 1024x1024 (~0.9 МБ): чашка -> тёмно-синяя, фон -> ночная кухня, яблоко/композиция/пар/тени сохранены |
Инструкция в редактировании выполнена выборочно — изменено запрошенное, остальное
осталось идентичным входному кадру.