From f5ec608fc2624f583c86847ba0c57d9b323711d5 Mon Sep 17 00:00:00 2001 From: Shaman Date: Sun, 4 Oct 2026 23:36:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 557 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 465 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index ddc9201..474bbcf 100644 --- a/README.md +++ b/README.md @@ -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 `): - -```json -{"prompt": "a red cube", "model": "z-image-turbo", "n": 1, "size": "1024x1024", "seed": 123} -``` - -- `n` поддерживается только 1 (иначе 400); `seed` необязателен — без него случайный - (повтор с тем же seed + prompt даёт идентичное изображение). -- Ответ: `{"created": , "data": [{"b64_json": ""}]}`. - -`POST /v1/images/edits` — два транспорта: - -- multipart: поля `image` (файл PNG/JPEG) + `prompt` (+ необязательные `model`, `seed`); -- JSON: `{"image": "", "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 +``` + +Отсутствие или несовпадение ключа -> `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": "" }] +} +``` + +### `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__.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 МБ): чашка -> тёмно-синяя, фон -> ночная кухня, яблоко/композиция/пар/тени сохранены | + +Инструкция в редактировании выполнена выборочно — изменено запрошенное, остальное +осталось идентичным входному кадру.