Krea2: VAE в edit-энкодере и латент под вход
Build and push Docker image / tests (push) Successful in 15s Details
Build and push Docker image / docker (push) Successful in 10s Details

Подключил VAE к TextEncodeQwenImageEdit, добавил латент под пропорции входного изображения. Поставил 10 шагов dpmpp_2m/karras.
This commit is contained in:
MpakobecEx 2026-10-20 01:07:07 +05:00
parent d4263c8420
commit 34806cf820
8 changed files with 106 additions and 10 deletions

View File

@ -285,8 +285,9 @@ docker compose up -d
- `workflows/txt2img.json` — Z-Image Turbo: `EmptySD3LatentImage → KSampler → VAEDecode → SaveImage`. - `workflows/txt2img.json` — Z-Image Turbo: `EmptySD3LatentImage → KSampler → VAEDecode → SaveImage`.
- `workflows/img2img.json` — Krea2 edit: `LoadImage → TextEncodeQwenImageEdit → KSamplerAdvanced → VAEDecode → SaveImage`. - `workflows/img2img.json` — Krea2 edit: `LoadImage → TextEncodeQwenImageEdit → KSamplerAdvanced → VAEDecode → SaveImage`.
- `workflows/mapping.json` — ID узлов, в которые подставляются prompt, negative, seed и входное изображение. - `workflows/mapping.json` — ID узлов, в которые подставляются prompt, negative, seed, входное изображение и размер латента.
- `workflows/Krea2.json` и `workflows/ZImage.json` — исходные экспорты. - `workflows/Krea2.json` и `workflows/ZImage.json` — исходные экспорты.
- Krea2 edit: VAE подключён к `TextEncodeQwenImageEdit`, латент подгоняется под пропорции входного изображения; 10 шагов, `dpmpp_2m`/`karras`.
Если меняются workflow, обновите `mapping.json`. При старте бот проверяет, что все узлы и поля существуют. Если меняются workflow, обновите `mapping.json`. При старте бот проверяет, что все узлы и поля существуют.

View File

@ -10,15 +10,23 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from PIL import Image, ImageOps
from comfy_client import ComfyClient, ComfyError from comfy_client import ComfyClient, ComfyError
from config import Settings from config import Settings
from db import Database, Request, UserAlreadyQueued from db import Database, Request, UserAlreadyQueued
from moderation import PromptBlocked, is_prompt_allowed from moderation import PromptBlocked, is_prompt_allowed
from prompt_enhancer import PromptEnhancer from prompt_enhancer import PromptEnhancer
from workflows import WorkflowBuilder from workflows import WorkflowBuilder, fit_latent_size
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _image_size(path: str) -> tuple[int, int]:
with Image.open(path) as image:
oriented = ImageOps.exif_transpose(image)
return fit_latent_size(*oriented.size)
EventCallback = Callable[["QueueEvent"], Awaitable[None] | None] EventCallback = Callable[["QueueEvent"], Awaitable[None] | None]
@ -362,11 +370,16 @@ class QueueManager:
suffix = input_path.suffix.lower() or ".png" suffix = input_path.suffix.lower() or ".png"
remote_name = f"{request.id}_input{suffix}" remote_name = f"{request.id}_input{suffix}"
image_name = await self._comfy.upload_image(input_path, remote_name) image_name = await self._comfy.upload_image(input_path, remote_name)
width, height = await asyncio.to_thread(
_image_size, request.input_file_path
)
return self._workflows.build_img2img( return self._workflows.build_img2img(
prompt=prompt, prompt=prompt,
negative=negative, negative=negative,
seed=seed, seed=seed,
image_name=image_name, image_name=image_name,
width=width,
height=height,
) )
async def _refresh(self, request_id: int) -> Request: async def _refresh(self, request_id: int) -> Request:

View File

@ -6,6 +6,7 @@ import time
from pathlib import Path from pathlib import Path
import pytest import pytest
from PIL import Image
from config import Settings from config import Settings
from db import Database, UserAlreadyQueued from db import Database, UserAlreadyQueued
@ -183,7 +184,7 @@ async def test_image_to_image_uploads_input(db: Database, tmp_path: Path) -> Non
comfy = queue._comfy # type: ignore[attr-defined] comfy = queue._comfy # type: ignore[attr-defined]
input_path = tmp_path / "input.png" input_path = tmp_path / "input.png"
input_path.write_bytes(b"input") Image.new("RGB", (64, 64), (120, 40, 40)).save(input_path)
await queue.start() await queue.start()
try: try:

View File

@ -5,7 +5,7 @@ from pathlib import Path
import pytest import pytest
from workflows import WorkflowBuilder, WorkflowError from workflows import WorkflowBuilder, WorkflowError, fit_latent_size
BASE = Path(__file__).resolve().parents[1] / "workflows" BASE = Path(__file__).resolve().parents[1] / "workflows"
@ -39,10 +39,50 @@ def test_build_img2img() -> None:
) )
assert workflow["25"]["inputs"]["prompt"] == "make blue" assert workflow["25"]["inputs"]["prompt"] == "make blue"
assert workflow["25"]["inputs"]["vae"] == ["19", 2]
assert workflow["10"]["inputs"]["text"] == "bad" assert workflow["10"]["inputs"]["text"] == "bad"
assert workflow["3"]["inputs"]["noise_seed"] == 7 assert workflow["3"]["inputs"]["noise_seed"] == 7
assert workflow["3"]["inputs"]["steps"] == 10
assert workflow["3"]["inputs"]["sampler_name"] == "dpmpp_2m"
assert workflow["3"]["inputs"]["scheduler"] == "karras"
assert workflow["20"]["class_type"] == "LoadImage" assert workflow["20"]["class_type"] == "LoadImage"
assert workflow["20"]["inputs"]["image"] == "input.png" assert workflow["20"]["inputs"]["image"] == "input.png"
assert workflow["15"]["inputs"]["width"] == 1024
assert workflow["15"]["inputs"]["height"] == 1024
def test_build_img2img_custom_size() -> None:
builder = make_builder()
workflow = builder.build_img2img(
prompt="make blue",
negative="bad",
seed=7,
image_name="input.png",
width=832,
height=1216,
)
assert workflow["15"]["inputs"]["width"] == 832
assert workflow["15"]["inputs"]["height"] == 1216
def test_fit_latent_size_preserves_aspect_ratio() -> None:
assert fit_latent_size(1024, 1024) == (1024, 1024)
width, height = fit_latent_size(1080, 1920)
assert width % 16 == 0
assert height % 16 == 0
assert height > width
assert max(width, height) <= 1536
width, height = fit_latent_size(4000, 3000)
assert width % 16 == 0
assert height % 16 == 0
assert width > height
def test_fit_latent_size_fallback_for_invalid_size() -> None:
assert fit_latent_size(0, 100) == (1024, 1024)
def test_templates_are_not_mutated() -> None: def test_templates_are_not_mutated() -> None:

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import copy import copy
import json import json
import math
from collections.abc import Callable from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -21,6 +22,7 @@ ROLE_FIELDS: dict[str, dict[str, RoleField]] = {
"negative": ("text", lambda value: isinstance(value, str)), "negative": ("text", lambda value: isinstance(value, str)),
"seed": ("noise_seed", lambda value: isinstance(value, int)), "seed": ("noise_seed", lambda value: isinstance(value, int)),
"input_image": ("image", lambda value: isinstance(value, str)), "input_image": ("image", lambda value: isinstance(value, str)),
"latent": (None, None),
"save": (None, None), "save": (None, None),
}, },
} }
@ -28,6 +30,31 @@ ROLE_FIELDS: dict[str, dict[str, RoleField]] = {
LATENT_CLASSES = {"EmptySD3LatentImage", "EmptyLatentImage", "EmptyLatentImagePresets"} LATENT_CLASSES = {"EmptySD3LatentImage", "EmptyLatentImage", "EmptyLatentImagePresets"}
def fit_latent_size(
width: int,
height: int,
*,
target_pixels: int = 1024 * 1024,
multiple: int = 16,
max_side: int = 1536,
) -> tuple[int, int]:
if width <= 0 or height <= 0:
return 1024, 1024
ratio = math.sqrt(target_pixels / (width * height))
new_width = width * ratio
new_height = height * ratio
longest = max(new_width, new_height)
if longest > max_side:
scale = max_side / longest
new_width *= scale
new_height *= scale
new_width = max(multiple, round(new_width / multiple) * multiple)
new_height = max(multiple, round(new_height / multiple) * multiple)
return int(new_width), int(new_height)
class WorkflowError(RuntimeError): class WorkflowError(RuntimeError):
pass pass
@ -109,6 +136,8 @@ class WorkflowBuilder:
negative: str, negative: str,
seed: int, seed: int,
image_name: str, image_name: str,
width: int = 1024,
height: int = 1024,
) -> dict[str, Any]: ) -> dict[str, Any]:
workflow = copy.deepcopy(self._templates["img2img"]) workflow = copy.deepcopy(self._templates["img2img"])
mapping = self._mapping["img2img"] mapping = self._mapping["img2img"]
@ -116,4 +145,7 @@ class WorkflowBuilder:
workflow[mapping["negative"]]["inputs"]["text"] = negative workflow[mapping["negative"]]["inputs"]["text"] = negative
workflow[mapping["seed"]]["inputs"]["noise_seed"] = seed workflow[mapping["seed"]]["inputs"]["noise_seed"] = seed
workflow[mapping["input_image"]]["inputs"]["image"] = image_name workflow[mapping["input_image"]]["inputs"]["image"] = image_name
latent = workflow[mapping["latent"]]["inputs"]
latent["width"] = width
latent["height"] = height
return workflow return workflow

View File

@ -3,10 +3,10 @@
"inputs": { "inputs": {
"add_noise": "enable", "add_noise": "enable",
"noise_seed": 782408632091183, "noise_seed": 782408632091183,
"steps": 8, "steps": 10,
"cfg": 1, "cfg": 1,
"sampler_name": "euler", "sampler_name": "dpmpp_2m",
"scheduler": "simple", "scheduler": "karras",
"start_at_step": 0, "start_at_step": 0,
"end_at_step": 10000, "end_at_step": 10000,
"return_with_leftover_noise": "disable", "return_with_leftover_noise": "disable",
@ -116,6 +116,10 @@
"19", "19",
1 1
], ],
"vae": [
"19",
2
],
"image": [ "image": [
"20", "20",
0 0

View File

@ -3,10 +3,10 @@
"inputs": { "inputs": {
"add_noise": "enable", "add_noise": "enable",
"noise_seed": 0, "noise_seed": 0,
"steps": 8, "steps": 10,
"cfg": 1, "cfg": 1,
"sampler_name": "euler", "sampler_name": "dpmpp_2m",
"scheduler": "simple", "scheduler": "karras",
"start_at_step": 0, "start_at_step": 0,
"end_at_step": 10000, "end_at_step": 10000,
"return_with_leftover_noise": "disable", "return_with_leftover_noise": "disable",
@ -115,6 +115,10 @@
"19", "19",
1 1
], ],
"vae": [
"19",
2
],
"image": [ "image": [
"20", "20",
0 0

View File

@ -11,6 +11,7 @@
"negative": "10", "negative": "10",
"seed": "3", "seed": "3",
"input_image": "20", "input_image": "20",
"latent": "15",
"save": "24" "save": "24"
} }
} }