diff --git a/checklist.md b/checklist.md index 4ce5924..e6bd681 100644 --- a/checklist.md +++ b/checklist.md @@ -17,54 +17,58 @@ | i2i без подписи | Отклонить и попросить промпт (не использовать дефолтный) | | Чаты | Приватные; `user_id` считается id чата | | 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.1 Экспорт workflow -- [ ] Открыть ComfyUI, включить **Settings → Dev mode Options**, чтобы появился пункт **Save (API Format)**. -- [ ] Собрать t2i-граф: `CheckpointLoaderSimple → CLIPTextEncode (positive) → CLIPTextEncode (negative) → KSampler → VAEDecode → SaveImage`. -- [ ] Собрать i2i-граф: `LoadImage → VAEEncode → KSampler (denoise 0.4–0.7) → VAEDecode → SaveImage`. -- [ ] Экспортировать оба графа кнопкой **Save (API Format)** в `workflows/txt2img_api.json` и `workflows/img2img_api.json`. -- [ ] Убедиться, что в JSON только API-узлы (`class_type`, `inputs`), без UI-метаданных и без `extra`-полей. -- [ ] Определить ID нужных узлов. Удобно: выделить узел → `Properties` → `Node ID`, либо найти по `class_type` в JSON (`CLIPTextEncode`, `KSampler`, `LoadImage`). -- [ ] Заполнить `workflows/mapping.json` (ID ниже — примеры, заменить на реальные): +- [x] Открыть ComfyUI, включить **Settings → Dev mode Options**, чтобы появился пункт **Save (API Format)**. +- [x] Собрать t2i-граф: `UnetLoaderGGUF → CLIPLoaderGGUF → CLIPTextEncode` + `EmptySD3LatentImage → KSampler → VAEDecode → SaveImage`. +- [x] Собрать i2i-граф: `LoadImage → TextEncodeQwenImageEdit → KSamplerAdvanced → VAEDecode → SaveImage`. +- [x] Экспортировать оба графа кнопкой **Save (API Format)** в `workflows/txt2img.json` и `workflows/img2img.json`. +- [x] Убедиться, что в JSON только API-узлы (`class_type`, `inputs`), без UI-метаданных и без `extra`-полей. +- [x] Определить ID нужных узлов. Удобно: выделить узел → `Properties` → `Node ID`, либо найти по `class_type` в JSON (`CLIPTextEncode`, `KSampler`, `LoadImage`). +- [x] Заполнить `workflows/mapping.json` (ID соответствуют текущим шаблонам): ```json { "txt2img": { - "positive": "6", - "negative": "7", - "seed": "3", - "checkpoint": "4" + "positive": "67", + "negative": "71", + "seed": "70", + "latent": "81", + "save": "9" }, "img2img": { - "positive": "6", - "negative": "7", + "positive": "25", + "negative": "10", "seed": "3", - "input_image": "10", - "denoise": "3" + "input_image": "20", + "save": "24" } } ``` -- [ ] Проверить t2i вручную: отправить JSON на `POST /prompt` и получить `prompt_id`, затем забрать результат через `/history` и `/view`. -- [ ] Проверить i2i: загрузить тестовое фото через `/upload/image`, подставить имя в `LoadImage`, выполнить генерацию. +- [x] Проверить t2i вручную: отправить JSON на `POST /prompt` и получить `prompt_id`, затем забрать результат через `/history` и `/view`. +- [x] Проверить i2i: загрузить тестовое фото через `/upload/image`, подставить имя в `LoadImage`, выполнить генерацию. - [ ] Проверить i2i с фото произвольного размера (портрет/ландшафт); при необходимости добавить `ImageScale` в граф. -- [ ] Зафиксировать дефолтные `steps`, `cfg`, `denoise`, negative prompt и размер изображения. +- [x] Зафиксировать дефолтные `steps`, `cfg`, `denoise`, negative prompt и размер изображения. ### 1.2 Окружение -- [ ] Убедиться, что Python 3.10+ доступен локально и в Docker-образе. -- [ ] Проверить сетевую доступность ComfyUI: `GET http://IP_WINDOWS:PORT/system_stats`. +- [x] Убедиться, что Python 3.10+ доступен локально и в Docker-образе. +- [x] Проверить сетевую доступность ComfyUI: `GET http://IP_WINDOWS:PORT/system_stats`. - [ ] Получить токен бота у `@BotFather`. - [ ] Определить значения лимитов: максимальная длина промпта, максимальный размер фото, размер очереди, таймаут генерации. - [ ] Решить, где будет запускаться бот (локально / 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. Каркас проекта и конфигурация @@ -78,8 +82,8 @@ ├── queue_manager.py ├── config.py ├── workflows/ -│ ├── txt2img_api.json -│ ├── img2img_api.json +│ ├── txt2img.json +│ ├── img2img.json │ └── mapping.json ├── storage/ # создаётся автоматически, в .gitignore ├── data/ # SQLite, в .gitignore @@ -161,11 +165,11 @@ ON requests(user_id) WHERE status IN ('queued', 'running'); ## 5. Шаблоны workflow -- [ ] Положить `txt2img_api.json` и `img2img_api.json` в `workflows/`. -- [ ] `workflows/mapping.json`: соответствие «роль узла → node id» для каждого workflow (positive, negative, seed, input_image, denoise). +- [x] Положить `txt2img.json` и `img2img.json` в `workflows/`. +- [x] `workflows/mapping.json`: соответствие «роль узла → node id» для каждого workflow (positive, negative, seed, latent/input_image). - [ ] Валидация mapping при старте: все указанные узлы существуют, у них ожидаемые поля; иначе — падение с понятной ошибкой. - [ ] `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 в БД. - [ ] Обрезка/нормализация промпта (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` и размер изображения? -2. Какая версия ComfyUI? В старых сборках событие `progress` может не содержать `prompt_id` — нужен другой способ корреляции. -3. i2i без подписи: отклонять (по умолчанию) или использовать фиксированный промпт? -4. Нужны ли `/cancel`, удаление из очереди и ограничение доступа `ALLOWED_USER_IDS` в MVP? -5. Сколько хранить входные/выходные файлы? Нужна ли автоочистка? -6. Где будет хоститься бот и как он видит ComfyUI (LAN/VPN/интернет)? -7. Добавлять ли в схему `mode`, `seed`, `error_text`, `finished_at` (сейчас они предложены как расширение плана)? -8. Уведомлять ли пользователя, если его `running`-задача была помечена `failed` после рестарта? +1. i2i без подписи: отклонять (по умолчанию) или использовать фиксированный промпт? +2. Нужны ли `/cancel`, удаление из очереди и ограничение доступа `ALLOWED_USER_IDS` в MVP? +3. Сколько хранить входные/выходные файлы? Нужна ли автоочистка? +4. Где будет хоститься бот и как он видит ComfyUI (LAN/VPN/интернет)? +5. Добавлять ли в схему `mode`, `seed`, `error_text`, `finished_at` (сейчас они предложены как расширение плана)? +6. Уведомлять ли пользователя, если его `running`-задача была помечена `failed` после рестарта? +7. Нужно ли масштабировать входное фото перед i2i (сейчас подаётся как есть; проверен только квадрат 1024×1024)? ## Вне скоупа MVP diff --git a/workflows/Krea2.json b/workflows/Krea2.json new file mode 100644 index 0000000..b8d39bc --- /dev/null +++ b/workflows/Krea2.json @@ -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" + } + } +} \ No newline at end of file diff --git a/workflows/ZImage.json b/workflows/ZImage.json new file mode 100644 index 0000000..ddc45cf --- /dev/null +++ b/workflows/ZImage.json @@ -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)" + } + } +} \ No newline at end of file diff --git a/workflows/img2img.json b/workflows/img2img.json new file mode 100644 index 0000000..3708c24 --- /dev/null +++ b/workflows/img2img.json @@ -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" + } + } +} diff --git a/workflows/mapping.json b/workflows/mapping.json new file mode 100644 index 0000000..61338b6 --- /dev/null +++ b/workflows/mapping.json @@ -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" + } +} diff --git a/workflows/txt2img.json b/workflows/txt2img.json new file mode 100644 index 0000000..255f0ef --- /dev/null +++ b/workflows/txt2img.json @@ -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" + } + } +}