Первая версия

This commit is contained in:
Shaman
2026-10-04 23:36:43 +03:00
parent c17ab5036e
commit f5ec608fc2
+465 -92
View File
@@ -1,134 +1,507 @@
# comfy-adapter
OpenAI-совместимый адаптер поверх ComfyUI: `POST /v1/images/generations` (генерация,
модель `z-image-turbo`) и `POST /v1/images/edits` (редактирование, модель
`qwen-image-edit`). Адаптер подставляет параметры запроса в шаблоны workflow,
отправляет их в ComfyUI, дождаться выполнения и возвращает изображение как
`data[0].b64_json` (base64 PNG).
**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-контрактом.
```
Client --(OpenAI contract)--> comfy-adapter --(ComfyUI HTTP API)--> ComfyUI
/v1/images/generations POST /prompt
/v1/images/edits POST /upload/image
GET /history/{id}
GET /view
+----------------+ 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 +----------------+
```
## Запуск
**Модели**
| id модели в API | Назначение | Диффузионная модель | Доп. |
|---|---|---|---|
| `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 как данные**: шаблоны JSON и карта маппинга параметров на узлы вынесены из кода.
- **Защита от деградации графа**: битые ссылки в шаблоне обнаруживаются локально и на сервере, адаптер не стартует с нерабочим workflow.
- **Статический API-ключ** на всех эндпоинтах, кроме `GET /health`.
- **Ошибки в формате OpenAI** (`error.message/type/param/code`) — клиенты корректно их разбирают.
- **Ответ всегда base64**: адаптеру и клиенту не нужен сетевой доступ к ComfyUI.
---
## Быстрый старт
### Локально
```bash
python -m venv .venv && .venv/bin/pip install -e . # Windows: .venv\Scripts\
cp .env.example .env # заполните COMFY_BASE_URL, ADAPTER_API_KEY
uvicorn app.main:app --host 0.0.0.0 --port 8000
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:
### Docker
```bash
docker build -t comfy-adapter .
docker run -p 8000:8000 --env-file .env comfy-adapter
```
При старте адаптер загружает `workflow_create.json` / `workflow_edit.json`, локально
проверяет графы на битые ссылки (битый шаблон — падение старта со списком проблемных
узлов) и прогоняет серверную проверку `POST /prompt?front=false` (отключается:
`VALIDATE_TEMPLATES_ON_SERVER=false`). Логи стартовой проверки видны с
`uvicorn --log-level info`.
## Переменные окружения
| Переменная | Default | Назначение |
|---|---|---|
| `COMFY_BASE_URL` | `http://192.168.0.3:8188` | Адрес ComfyUI |
| `ADAPTER_API_KEY` | — (обязательна) | Статический Bearer-ключ клиентов |
| `EXECUTION_TIMEOUT` | `300` | Секунд ожидания результата ComfyUI |
| `POLL_INTERVAL` | `1.5` | Интервал опроса `/history`, сек |
| `MAPPING_FILE` | — | JSON-файл, переопределяющий карту маппинга |
| `VALIDATE_TEMPLATES_ON_SERVER` | `true` | Проверять шаблоны на сервере при старте |
## Контракт
`POST /v1/images/generations` (JSON, `Authorization: Bearer <ADAPTER_API_KEY>`):
```json
{"prompt": "a red cube", "model": "z-image-turbo", "n": 1, "size": "1024x1024", "seed": 123}
```
- `n` поддерживается только 1 (иначе 400); `seed` необязателен — без него случайный
(повтор с тем же seed + prompt даёт идентичное изображение).
- Ответ: `{"created": <unix>, "data": [{"b64_json": "<base64 PNG>"}]}`.
`POST /v1/images/edits` — два транспорта:
- multipart: поля `image` (файл PNG/JPEG) + `prompt` (+ необязательные `model`, `seed`);
- JSON: `{"image": "<base64>", "prompt": "...", "model": "qwen-image-edit", "seed": 5}`.
Поле `size` в edits принимается, но не влияет на выход: итоговый размер — ~1 Мп с
аспектом входного изображения (это делает сам workflow).
Ошибки — в формате OpenAI (`error.message/type/param/code`): `400` — невалидное тело;
`401` — ключ отсутствует/неверен; `502` — ComfyUI недоступен, отверг workflow
(`comfyui_validation_failed`), ошибко исполнения (`comfyui_execution_failed`) или таймаут
(`comfyui_timeout`). `GET /health` — без ключа.
### Проверка
```bash
# генерация
curl -s http://localhost:8000/v1/images/generations \
-H "Authorization: Bearer $ADAPTER_API_KEY" -H "Content-Type: application/json" \
-d '{"prompt":"a red cube on white background","size":"1024x1024","seed":42}' | jq -r .data[0].b64_json | base64 -d > out.png
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`, сек |
| `CREATE_TEMPLATE_PATH` | `<репо>/workflow_create.json` | Путь к шаблону генерации |
| `EDIT_TEMPLATE_PATH` | `<репо>/workflow_edit.json` | Путь к шаблону редактирования |
| `MAPPING_FILE` | не задан | JSON с переопределением карты маппинга (см. ниже) |
| `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` | нет | Если задан — только `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 | нет | Только `qwen-image-edit` |
| `seed` | int | нет | В форме передаётся строкой, парсится в int |
**application/json:**
| Поле | Тип | Обязателен | Описание |
|---|---|---|---|
| `image` | string (base64) | **да** | Допускается data-URL-префикс `data:image/png;base64,...` |
| `prompt` | string | **да** | |
| `model` | string | нет | Только `qwen-image-edit` |
| `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=@input.png -F prompt="make the background deep blue" | jq -r .data[0].b64_json | base64 -d > edited.png
-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-...")
r = client.images.generate(model="z-image-turbo", prompt="a red cube", size="1024x1024", n=1)
open("out.png", "wb").write(__import__("base64").b64decode(r.data[0].b64_json))
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 — оба вызова проходят без кастомного транспорта.
Параметр -> узел workflow (правится без кода через `MAPPING_FILE`):
### JavaScript
| Параметр | workflow_create.json | workflow_edit.json |
```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/size/seed/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 не затронул бы остальной код.
### Карта маппинга
Соответствие параметра запроса узлу workflow. Правится **без изменения кода** через
`MAPPING_FILE` (JSON с секциями `create` и `edit`; заданные ключи перезаписывают
дефолт, остальные сохраняются).
| Параметр | `workflow_create.json` | `workflow_edit.json` |
|---|---|---|
| prompt | `83:27.inputs.text` | `115:111.inputs.prompt` |
| negative | — (ConditioningZeroOut) | `115:110.inputs.prompt` (всегда "") |
| size | `83:13.inputs.width/height` | — (размер = ~1 Мп входа) |
| seed | `83:3.inputs.seed` | `115:3.inputs.seed` |
| image | — | upload -> `78.inputs.image` |
| `prompt` | `83:27` -> `inputs.text` (CLIPTextEncode) | `115:111` -> `inputs.prompt` (TextEncodeQwenImageEditPlus) |
| negative | — (в графе `ConditioningZeroOut`, константа) | `115:110` -> `inputs.prompt` (всегда `""`) |
| `size` | `83:13` -> `inputs.width` / `inputs.height` (EmptySD3LatentImage) | — (см. «Ограничения») |
| `seed` | `83:3` -> `inputs.seed` (KSampler) | `115:3` -> `inputs.seed` (KSampler) |
| `image` | — | `78` -> `inputs.image` (LoadImage) |
## Починка шаблонов относительно экспорта OpenWebUI
Пример переопределения:
Выгруженные из OpenWebUI шаблоны **не проходили валидацию ComfyUI** (`KeyError` по
несуществующим узлам). Исправления в репозиторных файлах:
```json
{ "create": { "prompt": { "node": "99:1", "field": "text" } } }
```
- `workflow_create.json`: ссылки `[83:43, 0]`/`[83:43, 1]` (узла нет в графе)
переподключены на существующие лоадеры: `83:3.inputs.model -> [83:28, 0]`
(UNETLoader z_image_turbo), `83:27.inputs.clip -> [83:30, 0]` (CLIPLoader qwen_3_4b).
- `workflow_edit.json`: `115:66.inputs.model -> [115:89, 0]` вместо несуществующего
`115:117` — LoRA `Qwen-Image-Edit-2509-Lightning-4steps` (нода `115:89`, в экспорте
«висячая») подключена в цепочку модели.
При старте каждый id из карты проверяется на существование в шаблоне и на то, что
поле скалярное, — рассинхрон карты и шаблона падает на старт, а не на первый запрос.
Оба шаблона проходят `POST /prompt?front=false` и реально генерируют (проверено на
ComfyUI 0.37.0 / V100).
### Проверка шаблонов
## Замечания по продакшену
Двухуровневая, оба уровня на старте:
- Ответ синхронный: соединение висит до конца генерации. За reverse proxy поднимите
`proxy_read_timeout` (nginx) / эквивалент выше, чем `EXECUTION_TIMEOUT`.
- Фоновые интеграции (OpenWebUI) продолжают работать с ComfyUI напрямую — адаптер
ничего не migрирует.
1. **Локально** (`app/templates.py`): обход всех `inputs` и поиск ссылок
`[node_id, slot]` на несуществующие узлы. Найденное -> исключение со списком
`node X: input 'Y' links to missing node 'Z'`, адаптер не стартует.
2. **На сервере** (`VALIDATE_TEMPLATES_ON_SERVER=true`): `POST /prompt?front=false` —
ComfyUI валидирует граф, **не ставя задание в очередь** и не тратя GPU.
---
## Шаблоны workflow и их починка
Оба шаблона были выгружены из OpenWebUI (там прямая интеграция с ComfyUI). В
**исходном виде они не проходили валидацию ComfyUI** — в цепочках моделей
отсутствовали узлы, на которые шли ссылки:
```
workflow_create.json workflow_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_create.json` | `83:3.inputs.model = ["83:43", 0]`, `83:27.inputs.clip = ["83:43", 1]` | `["83:28", 0]` (UNETLoader), `["83:30", 0]` (CLIPLoader) |
| `workflow_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 сюда и сверить эти две
цепочки; локальная проверка на старте поймает проблему автоматически.
### Свойства шаблонов, которые стоит знать
- **Размер выхода в 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 + дефолтная карта маппинга
│ ├── comfy_client.py async-клиент ComfyUI: /prompt, /upload/image, /history, /view
│ ├── injector.py deep-copy шаблона + подстановка значений, парсинг 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, слияние MAPPING_FILE
│ ├── test_auth.py ключ: отсутствует / неверный / верный; health без ключа
│ ├── test_errors.py канонический вид error-body, соответствие статусов
│ ├── test_models.py формат /v1/models
│ └── test_live.py живые прогоны против реального ComfyUI (маркер live)
├── workflow_create.json исправленный шаблон генерации
├── workflow_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 # unit + mocked-ComfyUI (respx)
pytest -m live # живые прогоны: нужен запущенный адаптер и ADAPTER_LIVE=1,
# ADAPTER_URL=http://127.0.0.1:8000 ADAPTER_KEY=<ключ>
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` в шаблоне или
запускайте несколько запросов.
- **`size` в `/v1/images/edits` не влияет на выход** (см. «Свойства шаблонов»).
- **`negative_prompt` из запроса не принимается.** Узел в шаблоне есть, но поля в
схеме запроса нет — адаптер всегда пишет пустую строку.
- **Размер в генерации не квантуется.** Любое корректное `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 МБ): чашка -> тёмно-синяя, фон -> ночная кухня, яблоко/композиция/пар/тени сохранены |
Инструкция в редактировании выполнена выборочно — изменено запрошенное, остальное
осталось идентичным входному кадру.