OpenAI-compatible adapter for ComfyUI image generation and editing

This commit is contained in:
Shaman
2026-10-04 21:56:00 +03:00
commit c17ab5036e
29 changed files with 1730 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
# 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).
```
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
```
## Запуск
```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
```
Или 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/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
```
```python
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))
```
## Карта маппинга
Параметр -> узел workflow (правится без кода через `MAPPING_FILE`):
| Параметр | 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` |
## Починка шаблонов относительно экспорта OpenWebUI
Выгруженные из OpenWebUI шаблоны **не проходили валидацию ComfyUI** (`KeyError` по
несуществующим узлам). Исправления в репозиторных файлах:
- `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`, в экспорте
«висячая») подключена в цепочку модели.
Оба шаблона проходят `POST /prompt?front=false` и реально генерируют (проверено на
ComfyUI 0.37.0 / V100).
## Замечания по продакшену
- Ответ синхронный: соединение висит до конца генерации. За reverse proxy поднимите
`proxy_read_timeout` (nginx) / эквивалент выше, чем `EXECUTION_TIMEOUT`.
- Фоновые интеграции (OpenWebUI) продолжают работать с ComfyUI напрямую — адаптер
ничего не migрирует.
## Тесты
```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=<ключ>
```