# 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 ``` Отсутствие или несовпадение ключа -> `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": "" }] } ``` ### `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__.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 МБ): чашка -> тёмно-синяя, фон -> ночная кухня, яблоко/композиция/пар/тени сохранены | Инструкция в редактировании выполнена выборочно — изменено запрошенное, остальное осталось идентичным входному кадру.