feat: adapt ComfyUI t2i and i2i workflows

This commit is contained in:
MpakobecEx 2026-10-18 17:38:56 +05:00
parent d94e8dd48f
commit a5a520d7b9
6 changed files with 607 additions and 36 deletions

View File

@ -17,54 +17,58 @@
| i2i без подписи | Отклонить и попросить промпт (не использовать дефолтный) | | i2i без подписи | Отклонить и попросить промпт (не использовать дефолтный) |
| Чаты | Приватные; `user_id` считается id чата | | Чаты | Приватные; `user_id` считается id чата |
| Workflow | JSON в API-формате (экспорт «Save (API Format)» из ComfyUI) | | Workflow | JSON в API-формате (экспорт «Save (API Format)» из ComfyUI) |
| T2I-модель | Z-Image Turbo GGUF, `EmptySD3LatentImage`, 1024×1024, 8 steps, cfg 1, denoise 1.0 |
| I2I-модель | Krea2 edit GGUF, `LoadImage`, 8 steps, cfg 1 |
| ComfyUI | `http://192.168.31.113:8188`, v0.40.0, RTX 4060 Ti 8 GB |
| Язык интерфейса | Русский | | Язык интерфейса | Русский |
Согласовано: **1 воркер**, задача `running` при рестарте → **`failed`**, workflow готовим вместе (см. блок 1). Согласовано: **1 воркер**, задача `running` при рестарте → **`failed`**, workflow готовим вместе (см. блок 1). Для i2i используем **Krea2 edit**.
## 1. Подготовка workflow и окружения (до кода) ## 1. Подготовка workflow и окружения (до кода)
### 1.1 Экспорт workflow ### 1.1 Экспорт workflow
- [ ] Открыть ComfyUI, включить **Settings → Dev mode Options**, чтобы появился пункт **Save (API Format)**. - [x] Открыть ComfyUI, включить **Settings → Dev mode Options**, чтобы появился пункт **Save (API Format)**.
- [ ] Собрать t2i-граф: `CheckpointLoaderSimple → CLIPTextEncode (positive) → CLIPTextEncode (negative) → KSampler → VAEDecode → SaveImage`. - [x] Собрать t2i-граф: `UnetLoaderGGUF → CLIPLoaderGGUF → CLIPTextEncode` + `EmptySD3LatentImage → KSampler → VAEDecode → SaveImage`.
- [ ] Собрать i2i-граф: `LoadImage → VAEEncode → KSampler (denoise 0.4–0.7) → VAEDecode → SaveImage`. - [x] Собрать i2i-граф: `LoadImage → TextEncodeQwenImageEdit → KSamplerAdvanced → VAEDecode → SaveImage`.
- [ ] Экспортировать оба графа кнопкой **Save (API Format)** в `workflows/txt2img_api.json` и `workflows/img2img_api.json`. - [x] Экспортировать оба графа кнопкой **Save (API Format)** в `workflows/txt2img.json` и `workflows/img2img.json`.
- [ ] Убедиться, что в JSON только API-узлы (`class_type`, `inputs`), без UI-метаданных и без `extra`-полей. - [x] Убедиться, что в JSON только API-узлы (`class_type`, `inputs`), без UI-метаданных и без `extra`-полей.
- [ ] Определить ID нужных узлов. Удобно: выделить узел → `Properties` → `Node ID`, либо найти по `class_type` в JSON (`CLIPTextEncode`, `KSampler`, `LoadImage`). - [x] Определить ID нужных узлов. Удобно: выделить узел → `Properties` → `Node ID`, либо найти по `class_type` в JSON (`CLIPTextEncode`, `KSampler`, `LoadImage`).
- [ ] Заполнить `workflows/mapping.json` (ID ниже — примеры, заменить на реальные): - [x] Заполнить `workflows/mapping.json` (ID соответствуют текущим шаблонам):
```json ```json
{ {
"txt2img": { "txt2img": {
"positive": "6", "positive": "67",
"negative": "7", "negative": "71",
"seed": "3", "seed": "70",
"checkpoint": "4" "latent": "81",
"save": "9"
}, },
"img2img": { "img2img": {
"positive": "6", "positive": "25",
"negative": "7", "negative": "10",
"seed": "3", "seed": "3",
"input_image": "10", "input_image": "20",
"denoise": "3" "save": "24"
} }
} }
``` ```
- [ ] Проверить t2i вручную: отправить JSON на `POST /prompt` и получить `prompt_id`, затем забрать результат через `/history` и `/view`. - [x] Проверить t2i вручную: отправить JSON на `POST /prompt` и получить `prompt_id`, затем забрать результат через `/history` и `/view`.
- [ ] Проверить i2i: загрузить тестовое фото через `/upload/image`, подставить имя в `LoadImage`, выполнить генерацию. - [x] Проверить i2i: загрузить тестовое фото через `/upload/image`, подставить имя в `LoadImage`, выполнить генерацию.
- [ ] Проверить i2i с фото произвольного размера (портрет/ландшафт); при необходимости добавить `ImageScale` в граф. - [ ] Проверить i2i с фото произвольного размера (портрет/ландшафт); при необходимости добавить `ImageScale` в граф.
- [ ] Зафиксировать дефолтные `steps`, `cfg`, `denoise`, negative prompt и размер изображения. - [x] Зафиксировать дефолтные `steps`, `cfg`, `denoise`, negative prompt и размер изображения.
### 1.2 Окружение ### 1.2 Окружение
- [ ] Убедиться, что Python 3.10+ доступен локально и в Docker-образе. - [x] Убедиться, что Python 3.10+ доступен локально и в Docker-образе.
- [ ] Проверить сетевую доступность ComfyUI: `GET http://IP_WINDOWS:PORT/system_stats`. - [x] Проверить сетевую доступность ComfyUI: `GET http://IP_WINDOWS:PORT/system_stats`.
- [ ] Получить токен бота у `@BotFather`. - [ ] Получить токен бота у `@BotFather`.
- [ ] Определить значения лимитов: максимальная длина промпта, максимальный размер фото, размер очереди, таймаут генерации. - [ ] Определить значения лимитов: максимальная длина промпта, максимальный размер фото, размер очереди, таймаут генерации.
- [ ] Решить, где будет запускаться бот (локально / Docker) и как он видит ComfyUI (LAN, VPN, `host.docker.internal`). - [ ] Решить, где будет запускаться бот (локально / Docker) и как он видит ComfyUI (LAN, VPN, `host.docker.internal`).
**DoD:** есть токен, два валидных workflow JSON, заполненный `mapping.json`; тестовый `POST /prompt` возвращает `prompt_id`, результат скачивается. **DoD:** есть токен, два валидных workflow JSON, заполненный `mapping.json`; тестовый `POST /prompt` возвращает `prompt_id`, результат скачивается. T2I проверен (~52 с), i2i — (~61 с).
## 2. Каркас проекта и конфигурация ## 2. Каркас проекта и конфигурация
@ -78,8 +82,8 @@
├── queue_manager.py ├── queue_manager.py
├── config.py ├── config.py
├── workflows/ ├── workflows/
│ ├── txt2img_api.json │ ├── txt2img.json
│ ├── img2img_api.json │ ├── img2img.json
│ └── mapping.json │ └── mapping.json
├── storage/ # создаётся автоматически, в .gitignore ├── storage/ # создаётся автоматически, в .gitignore
├── data/ # SQLite, в .gitignore ├── data/ # SQLite, в .gitignore
@ -161,11 +165,11 @@ ON requests(user_id) WHERE status IN ('queued', 'running');
## 5. Шаблоны workflow ## 5. Шаблоны workflow
- [ ] Положить `txt2img_api.json` и `img2img_api.json` в `workflows/`. - [x] Положить `txt2img.json` и `img2img.json` в `workflows/`.
- [ ] `workflows/mapping.json`: соответствие «роль узла → node id» для каждого workflow (positive, negative, seed, input_image, denoise). - [x] `workflows/mapping.json`: соответствие «роль узла → node id» для каждого workflow (positive, negative, seed, latent/input_image).
- [ ] Валидация mapping при старте: все указанные узлы существуют, у них ожидаемые поля; иначе — падение с понятной ошибкой. - [ ] Валидация mapping при старте: все указанные узлы существуют, у них ожидаемые поля; иначе — падение с понятной ошибкой.
- [ ] `build_txt2img(prompt, negative, seed) -> dict`: глубокое копирование шаблона, подстановка prompt/negative/seed. - [ ] `build_txt2img(prompt, negative, seed) -> dict`: глубокое копирование шаблона, подстановка prompt/negative/seed.
- [ ] `build_img2img(prompt, negative, seed, image_name, denoise) -> dict`: то же + имя загруженного изображения. - [ ] `build_img2img(prompt, negative, seed, image_name) -> dict`: то же + имя загруженного изображения.
- [ ] Генерация seed: `secrets.randbelow(2**32)`, сохранять использованный seed в БД. - [ ] Генерация seed: `secrets.randbelow(2**32)`, сохранять использованный seed в БД.
- [ ] Обрезка/нормализация промпта (strip, лимит длины), значение negative prompt по умолчанию из конфига. - [ ] Обрезка/нормализация промпта (strip, лимит длины), значение negative prompt по умолчанию из конфига.
- [ ] Гарантировать, что исходный шаблон не мутируется между запросами. - [ ] Гарантировать, что исходный шаблон не мутируется между запросами.
@ -290,16 +294,15 @@ ON requests(user_id) WHERE status IN ('queued', 'running');
## Открытые вопросы ## Открытые вопросы
Согласовано: 1 воркер; `running` при рестарте → `failed`; workflow готовим вместе. Осталось уточнить: Согласовано: 1 воркер; `running` при рестарте → `failed`; T2I — Z-Image, i2i — Krea2 edit. Осталось уточнить:
1. Какой чекпоинт/модель используется (SD 1.5, SDXL, Flux) и какие дефолтные `steps`/`cfg`/`denoise` и размер изображения? 1. i2i без подписи: отклонять (по умолчанию) или использовать фиксированный промпт?
2. Какая версия ComfyUI? В старых сборках событие `progress` может не содержать `prompt_id` — нужен другой способ корреляции. 2. Нужны ли `/cancel`, удаление из очереди и ограничение доступа `ALLOWED_USER_IDS` в MVP?
3. i2i без подписи: отклонять (по умолчанию) или использовать фиксированный промпт? 3. Сколько хранить входные/выходные файлы? Нужна ли автоочистка?
4. Нужны ли `/cancel`, удаление из очереди и ограничение доступа `ALLOWED_USER_IDS` в MVP? 4. Где будет хоститься бот и как он видит ComfyUI (LAN/VPN/интернет)?
5. Сколько хранить входные/выходные файлы? Нужна ли автоочистка? 5. Добавлять ли в схему `mode`, `seed`, `error_text`, `finished_at` (сейчас они предложены как расширение плана)?
6. Где будет хоститься бот и как он видит ComfyUI (LAN/VPN/интернет)? 6. Уведомлять ли пользователя, если его `running`-задача была помечена `failed` после рестарта?
7. Добавлять ли в схему `mode`, `seed`, `error_text`, `finished_at` (сейчас они предложены как расширение плана)? 7. Нужно ли масштабировать входное фото перед i2i (сейчас подаётся как есть; проверен только квадрат 1024×1024)?
8. Уведомлять ли пользователя, если его `running`-задача была помечена `failed` после рестарта?
## Вне скоупа MVP ## Вне скоупа MVP

129
workflows/Krea2.json Normal file
View File

@ -0,0 +1,129 @@
{
"3": {
"inputs": {
"add_noise": "enable",
"noise_seed": 782408632091183,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 10000,
"return_with_leftover_noise": "disable",
"model": [
"19",
0
],
"positive": [
"25",
0
],
"negative": [
"10",
0
],
"latent_image": [
"15",
0
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"10": {
"inputs": {
"text": "low quality",
"clip": [
"19",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"11": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"19",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"15": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 2
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"19": {
"inputs": {
"unet_name": "Krea2_turbo_uncensored_edit_v1.1-Q4_K_M.gguf",
"clip_name": "qwen3vl_4b_fp8_scaled.safetensors",
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "Krea2ModelLoader",
"_meta": {
"title": "Krea2 Model Loader ⚡️"
}
},
"20": {
"inputs": {
"image": "ComfyUI_00077.png [output]",
"refresh": "refresh"
},
"class_type": "LoadImageOutput",
"_meta": {
"title": "Load Image (from Outputs)"
}
},
"24": {
"inputs": {
"filename_prefix": "ComfyUI",
"format": "png",
"format.bit_depth": "8-bit",
"format.input_color_space": "sRGB",
"images": [
"11",
0
]
},
"class_type": "SaveImageAdvanced",
"_meta": {
"title": "Save Image (Advanced)"
}
},
"25": {
"inputs": {
"prompt": "Не меняй ничего. Оставь позу и ракуср без изменений.Нарисуй парню красивый член",
"clip": [
"19",
1
],
"image": [
"20",
0
]
},
"class_type": "TextEncodeQwenImageEdit",
"_meta": {
"title": "Text Encode Qwen Image Edit"
}
}
}

155
workflows/ZImage.json Normal file
View File

@ -0,0 +1,155 @@
{
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"65",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"63": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"65": {
"inputs": {
"samples": [
"70",
0
],
"vae": [
"63",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"67": {
"inputs": {
"text": "two characters, composition inspired by reference, swap their positions: left character now on the right, right character now on the left, same art style, same lighting",
"clip": [
"73",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"69": {
"inputs": {
"shift": 3,
"sampling": "flow",
"model": [
"72",
0
]
},
"class_type": "ModelSamplingAuraFlow",
"_meta": {
"title": "ModelSamplingAuraFlow"
}
},
"70": {
"inputs": {
"seed": 42,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "sgm_uniform",
"denoise": 0.67,
"model": [
"69",
0
],
"positive": [
"67",
0
],
"negative": [
"71",
0
],
"latent_image": [
"81",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"71": {
"inputs": {
"text": "low quality, bad anatomy, extra digits, missing digits, extra limbs, missing limbs",
"clip": [
"73",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"72": {
"inputs": {
"unet_name": "z_image_turbo_fp32-Q4_K_M.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "UNET Loader (GGUF) ⚡️"
}
},
"73": {
"inputs": {
"clip_name": "Qwen3_4B.Q4_K_M.gguf",
"type": "lumina2"
},
"class_type": "CLIPLoaderGGUF",
"_meta": {
"title": "CLIP Loader (GGUF) ⚡️"
}
},
"81": {
"inputs": {
"pixels": [
"82",
0
],
"vae": [
"63",
0
]
},
"class_type": "VAEEncode",
"_meta": {
"title": "VAE Encode"
}
},
"82": {
"inputs": {
"image": "ComfyUI_00034_.png [output]",
"refresh": "refresh"
},
"class_type": "LoadImageOutput",
"_meta": {
"title": "Load Image (from Outputs)"
}
}
}

128
workflows/img2img.json Normal file
View File

@ -0,0 +1,128 @@
{
"3": {
"inputs": {
"add_noise": "enable",
"noise_seed": 0,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "simple",
"start_at_step": 0,
"end_at_step": 10000,
"return_with_leftover_noise": "disable",
"model": [
"19",
0
],
"positive": [
"25",
0
],
"negative": [
"10",
0
],
"latent_image": [
"15",
0
]
},
"class_type": "KSamplerAdvanced",
"_meta": {
"title": "KSampler (Advanced)"
}
},
"10": {
"inputs": {
"text": "__NEGATIVE__",
"clip": [
"19",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"11": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"19",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"15": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"19": {
"inputs": {
"unet_name": "Krea2_turbo_uncensored_edit_v1.1-Q4_K_M.gguf",
"clip_name": "qwen3vl_4b_fp8_scaled.safetensors",
"vae_name": "qwen_image_vae.safetensors"
},
"class_type": "Krea2ModelLoader",
"_meta": {
"title": "Krea2 Model Loader ⚡️"
}
},
"20": {
"class_type": "LoadImage",
"inputs": {
"image": "__INPUT_IMAGE__"
},
"_meta": {
"title": "Load Image"
}
},
"24": {
"inputs": {
"filename_prefix": "Mpako",
"format": "png",
"format.bit_depth": "8-bit",
"format.input_color_space": "sRGB",
"images": [
"11",
0
]
},
"class_type": "SaveImageAdvanced",
"_meta": {
"title": "Save Image (Advanced)"
}
},
"25": {
"inputs": {
"prompt": "__PROMPT__",
"clip": [
"19",
1
],
"image": [
"20",
0
]
},
"class_type": "TextEncodeQwenImageEdit",
"_meta": {
"title": "Text Encode Qwen Image Edit"
}
}
}

16
workflows/mapping.json Normal file
View File

@ -0,0 +1,16 @@
{
"txt2img": {
"positive": "67",
"negative": "71",
"seed": "70",
"latent": "81",
"save": "9"
},
"img2img": {
"positive": "25",
"negative": "10",
"seed": "3",
"input_image": "20",
"save": "24"
}
}

140
workflows/txt2img.json Normal file
View File

@ -0,0 +1,140 @@
{
"9": {
"inputs": {
"filename_prefix": "Mpako",
"images": [
"65",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
},
"63": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "Load VAE"
}
},
"65": {
"inputs": {
"samples": [
"70",
0
],
"vae": [
"63",
0
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"67": {
"inputs": {
"text": "__PROMPT__",
"clip": [
"73",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"69": {
"inputs": {
"shift": 3,
"sampling": "flow",
"model": [
"72",
0
]
},
"class_type": "ModelSamplingAuraFlow",
"_meta": {
"title": "ModelSamplingAuraFlow"
}
},
"70": {
"inputs": {
"seed": 0,
"steps": 8,
"cfg": 1,
"sampler_name": "euler",
"scheduler": "sgm_uniform",
"denoise": 1.0,
"model": [
"69",
0
],
"positive": [
"67",
0
],
"negative": [
"71",
0
],
"latent_image": [
"81",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"71": {
"inputs": {
"text": "__NEGATIVE__",
"clip": [
"73",
0
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"72": {
"inputs": {
"unet_name": "z_image_turbo_fp32-Q4_K_M.gguf"
},
"class_type": "UnetLoaderGGUF",
"_meta": {
"title": "UNET Loader (GGUF) ⚡️"
}
},
"73": {
"inputs": {
"clip_name": "Qwen3_4B.Q4_K_M.gguf",
"type": "lumina2"
},
"class_type": "CLIPLoaderGGUF",
"_meta": {
"title": "CLIP Loader (GGUF) ⚡️"
}
},
"81": {
"class_type": "EmptySD3LatentImage",
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"_meta": {
"title": "Empty Latent"
}
}
}