feat: Flake Inpaint chain node feeding Flake Generate (#428)
This commit is contained in:
@@ -316,6 +316,22 @@ at full size and scrolls both ways. Landscape outputs widen the overlay to
|
||||
nearly the screen width. Prompts,
|
||||
LoRAs and model details are folded under the `^` at the bottom of each panel.
|
||||
|
||||
### Flake Inpaint
|
||||
|
||||
Insert it right before Flake Generate to redraw part of an image with the
|
||||
chain's model, LoRAs and prompt. Load the image on the node and paint the area
|
||||
to redraw with ComfyUI's mask editor (right-click → Open in MaskEditor). The
|
||||
image's size replaces the chain resolution.
|
||||
|
||||
- **denoise** `1.0` fills the masked area from scratch (grown by `grow_mask`);
|
||||
below `1.0` it keeps the original as a starting point. Flake Generate samples
|
||||
at this denoise.
|
||||
- Without a mask the whole image is redrawn at `denoise` (img2img).
|
||||
- `positive_prompt` / `negative_prompt` are appended to the chain prompt.
|
||||
|
||||
To fix an image Flakes made, drop it into ComfyUI to restore its workflow,
|
||||
insert Flake Inpaint before Flake Generate and load the same image.
|
||||
|
||||
### Flake Data Split
|
||||
|
||||
TODO
|
||||
|
||||
+10
-5
@@ -86,7 +86,7 @@ def _split_flake_data(flake_data):
|
||||
)
|
||||
else:
|
||||
filename_prefix = ""
|
||||
steps, cfg, sampler_name, scheduler = sampling_preset
|
||||
steps, cfg, sampler_name, scheduler, denoise = sampling_preset
|
||||
return {
|
||||
"model": model,
|
||||
"clip": clip,
|
||||
@@ -101,6 +101,7 @@ def _split_flake_data(flake_data):
|
||||
"cfg": cfg,
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"denoise": denoise,
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +197,7 @@ class IntoFlakeDataAll:
|
||||
else:
|
||||
generation_data = None
|
||||
|
||||
sampling_preset = (steps, cfg, sampler_name, scheduler)
|
||||
sampling_preset = (steps, cfg, sampler_name, scheduler, 1.0)
|
||||
|
||||
return ((model_bundle, generation_data, sampling_preset),)
|
||||
|
||||
@@ -246,7 +247,7 @@ class IntoFlakeDataSelect:
|
||||
orig_filename_state = {}
|
||||
if len(orig_generation_data) > 7 and isinstance(orig_generation_data[7], dict):
|
||||
orig_filename_state = orig_generation_data[7]
|
||||
orig_steps, orig_cfg, orig_sampler, orig_scheduler = orig_sampling_preset
|
||||
orig_steps, orig_cfg, orig_sampler, orig_scheduler, orig_denoise = orig_sampling_preset
|
||||
|
||||
new_model_bundle = (
|
||||
model if model is not None else orig_model,
|
||||
@@ -272,6 +273,7 @@ class IntoFlakeDataSelect:
|
||||
cfg if cfg is not None else orig_cfg,
|
||||
sampler_name if sampler_name is not None else orig_sampler,
|
||||
scheduler if scheduler is not None else orig_scheduler,
|
||||
orig_denoise,
|
||||
)
|
||||
|
||||
return ((new_model_bundle, new_generation_data, new_sampling_preset),)
|
||||
@@ -349,11 +351,12 @@ class PreviewFlakeData:
|
||||
|
||||
# --- Parameters ---
|
||||
if sampling_preset is not None:
|
||||
steps, cfg, sampler_name, scheduler = sampling_preset
|
||||
steps, cfg, sampler_name, scheduler, denoise = sampling_preset
|
||||
params_info["Steps"] = str(steps)
|
||||
params_info["CFG"] = str(cfg)
|
||||
params_info["Sampler"] = str(sampler_name)
|
||||
params_info["Scheduler"] = str(scheduler)
|
||||
params_info["Denoise"] = str(denoise)
|
||||
|
||||
# --- Metadata ---
|
||||
if generation_data is not None:
|
||||
@@ -495,6 +498,7 @@ class FlakeGenerate:
|
||||
cfg = parts["cfg"]
|
||||
sampler_name = parts["sampler_name"]
|
||||
scheduler = parts["scheduler"]
|
||||
denoise = parts["denoise"]
|
||||
|
||||
# Distilled models sample at cfg 1 with a zeroed negative (#359). Done
|
||||
# here rather than at preset-load time because this is the one place the
|
||||
@@ -513,7 +517,7 @@ class FlakeGenerate:
|
||||
ks = KSampler()
|
||||
sampled = ks.sample(
|
||||
model, seed, steps, cfg, sampler_name, scheduler,
|
||||
positive, negative, latent, denoise=1.0,
|
||||
positive, negative, latent, denoise=denoise,
|
||||
)
|
||||
sampled_latent = sampled[0]
|
||||
|
||||
@@ -630,6 +634,7 @@ class FlakeGenerate:
|
||||
models_info["CFG"] = str(cfg)
|
||||
models_info["Sampler"] = str(sampler_name)
|
||||
models_info["Scheduler"] = str(scheduler)
|
||||
models_info["Denoise"] = str(denoise)
|
||||
|
||||
# ── Inputs overlay: prompts (grouped by BREAK) + controlnet info ──
|
||||
inputs_info = {}
|
||||
|
||||
+54
-39
@@ -1,11 +1,16 @@
|
||||
"""Extra Flake nodes that wrap optional, soft-dependency features:
|
||||
IPAdapter FaceID (#289) and Inpaint (#291).
|
||||
IPAdapter FaceID (#289) and Inpaint (#291, #428).
|
||||
|
||||
These operate on the FLAKE_DATA bundle (model_bundle, generation_data,
|
||||
sampling_preset) produced by FlakeModelPreset / FlakeStack.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from nodes import CLIPTextEncode, LoadImage, SetLatentNoiseMask, VAEEncode, VAEEncodeForInpaint
|
||||
|
||||
from .flake_compose import carry_schedules
|
||||
from .full_flake_node import _carry_controlnets
|
||||
|
||||
|
||||
class FlakeIPAdapter:
|
||||
"""Apply IPAdapter FaceID to the model inside a FLAKE_DATA bundle for
|
||||
@@ -45,17 +50,20 @@ class FlakeIPAdapter:
|
||||
return (((new_model, clip, vae), generation_data, sampling_preset),)
|
||||
|
||||
|
||||
def _reencode(clip, cond, text):
|
||||
"""Encode ``text`` and carry the hooks, scheduled prompts and ControlNets
|
||||
of ``cond`` onto it, like FlakeStack does."""
|
||||
new_cond = CLIPTextEncode().encode(clip, text)[0]
|
||||
return _carry_controlnets(cond, carry_schedules(cond, new_cond, [], []))
|
||||
|
||||
|
||||
class FlakeInpaint:
|
||||
"""Inpaint a masked region of an image using a FLAKE_DATA bundle's model,
|
||||
conditioning, and sampler settings (#291).
|
||||
"""Put an image into a FLAKE_DATA bundle so Flake Generate inpaints it
|
||||
(#291, #428).
|
||||
|
||||
Takes an input image + mask (wire a Load Image / mask source) and the
|
||||
generation parameters from FLAKE_DATA. Optional prompt overrides re-encode
|
||||
using the bundle's CLIP. Outputs the inpainted IMAGE.
|
||||
|
||||
NOTE: the draft's on-node image preview with Mask/Inputs/Remove hover
|
||||
buttons (canvas mask editor) is a frontend follow-up; this node provides the
|
||||
working inpaint backend via input pins in the meantime.
|
||||
The mask is the image's alpha channel, painted with ComfyUI's mask editor
|
||||
on the node. Without a mask the whole image is redrawn at ``denoise``.
|
||||
Optional prompts are appended to the chain prompt.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
@@ -63,48 +71,55 @@ class FlakeInpaint:
|
||||
return {
|
||||
"required": {
|
||||
"flake_data": ("FLAKE_DATA",),
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff, "control_after_generate": True}),
|
||||
"image": LoadImage.INPUT_TYPES()["required"]["image"],
|
||||
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
"grow_mask": ("INT", {"default": 6, "min": 0, "max": 64,
|
||||
"tooltip": "Grow the mask by this many pixels. Used at denoise 1."}),
|
||||
},
|
||||
"optional": {
|
||||
"denoise": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"grow_mask": ("INT", {"default": 6, "min": 0, "max": 256}),
|
||||
"positive_prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"negative_prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
RETURN_TYPES = ("FLAKE_DATA",)
|
||||
RETURN_NAMES = ("flake_data",)
|
||||
FUNCTION = "execute"
|
||||
CATEGORY = "flakes"
|
||||
DESCRIPTION = (
|
||||
"Inpaint a masked region using the model, conditioning, and sampler "
|
||||
"settings from a FLAKE_DATA bundle. Optional prompt overrides."
|
||||
"Inpaint the masked area of an image: insert before Flake Generate. Paint "
|
||||
"the mask with the mask editor; without one the whole image is redrawn at denoise."
|
||||
)
|
||||
|
||||
def execute(self, flake_data, image, mask, seed, denoise=0.7, grow_mask=6,
|
||||
def execute(self, flake_data, image, denoise=1.0, grow_mask=6,
|
||||
positive_prompt="", negative_prompt=""):
|
||||
from nodes import VAEEncodeForInpaint, KSampler, VAEDecode, CLIPTextEncode
|
||||
|
||||
model_bundle, generation_data, sampling_preset = flake_data
|
||||
model, clip, vae = model_bundle
|
||||
positive, negative = generation_data[0], generation_data[1]
|
||||
steps, cfg, sampler_name, scheduler = sampling_preset
|
||||
_, clip, vae = model_bundle
|
||||
positive, negative, _, _, _, pos_text, neg_text = generation_data[:7]
|
||||
|
||||
# Optional prompt overrides — re-encode with the bundle's CLIP.
|
||||
if positive_prompt.strip() or negative_prompt.strip():
|
||||
encoder = CLIPTextEncode()
|
||||
if positive_prompt.strip():
|
||||
positive = encoder.encode(clip, positive_prompt)[0]
|
||||
if negative_prompt.strip():
|
||||
negative = encoder.encode(clip, negative_prompt)[0]
|
||||
pixels, mask = LoadImage().load_image(image)
|
||||
if not mask.any():
|
||||
latent = VAEEncode().encode(vae, pixels)[0]
|
||||
elif denoise < 1.0:
|
||||
latent = SetLatentNoiseMask().set_mask(VAEEncode().encode(vae, pixels)[0], mask)[0]
|
||||
else:
|
||||
latent = VAEEncodeForInpaint().encode(vae, pixels, mask, grow_mask_by=grow_mask)[0]
|
||||
|
||||
latent = VAEEncodeForInpaint().encode(vae, image, mask, grow_mask_by=grow_mask)[0]
|
||||
sampled = KSampler().sample(
|
||||
model, seed, steps, cfg, sampler_name, scheduler,
|
||||
positive, negative, latent, denoise=denoise,
|
||||
)[0]
|
||||
out_image = VAEDecode().decode(vae, sampled)[0]
|
||||
return (out_image,)
|
||||
if positive_prompt.strip():
|
||||
pos_text = " BREAK ".join(t for t in (pos_text.strip(), positive_prompt.strip()) if t)
|
||||
positive = _reencode(clip, positive, pos_text)
|
||||
if negative_prompt.strip():
|
||||
neg_text = ", ".join(t for t in (neg_text.strip(), negative_prompt.strip()) if t)
|
||||
negative = _reencode(clip, negative, neg_text)
|
||||
|
||||
generation_data = (positive, negative, latent, pixels.shape[2], pixels.shape[1],
|
||||
pos_text, neg_text) + tuple(generation_data[7:])
|
||||
return ((model_bundle, generation_data, sampling_preset[:4] + (denoise,)),)
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, image, **kwargs):
|
||||
return LoadImage.IS_CHANGED(image)
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, image):
|
||||
return LoadImage.VALIDATE_INPUTS(image)
|
||||
|
||||
+5
-5
@@ -306,7 +306,7 @@ def _load_preset_bundle(preset_name: str, model_family: str | None = None):
|
||||
"model_family": model_family,
|
||||
}
|
||||
generation_data = (positive, negative, latent, width, height, pos_text, neg_text, filename_state)
|
||||
sampling_preset = (preset_data.steps, preset_data.cfg, preset_data.sampler, preset_data.scheduler)
|
||||
sampling_preset = (preset_data.steps, preset_data.cfg, preset_data.sampler, preset_data.scheduler, 1.0)
|
||||
|
||||
return model_bundle, generation_data, sampling_preset
|
||||
|
||||
@@ -327,7 +327,7 @@ def _apply_preset_overrides(overrides_json, model_bundle, generation_data, sampl
|
||||
if not isinstance(ovr, dict) or not ovr:
|
||||
return model_bundle, generation_data, sampling_preset
|
||||
|
||||
steps, cfg, sampler, scheduler = sampling_preset
|
||||
steps, cfg, sampler, scheduler, denoise = sampling_preset
|
||||
|
||||
def _conv(key, conv, cur):
|
||||
v = ovr.get(key)
|
||||
@@ -344,7 +344,7 @@ def _apply_preset_overrides(overrides_json, model_bundle, generation_data, sampl
|
||||
sampler = str(ovr["sampler"])
|
||||
if ovr.get("scheduler"):
|
||||
scheduler = str(ovr["scheduler"])
|
||||
sampling_preset = (steps, cfg, sampler, scheduler)
|
||||
sampling_preset = (steps, cfg, sampler, scheduler, denoise)
|
||||
|
||||
fp = (ovr.get("filename_prefix") or "").strip()
|
||||
if fp and isinstance(generation_data, tuple) and len(generation_data) > 7 and isinstance(generation_data[7], dict):
|
||||
@@ -453,7 +453,7 @@ class FlakeStack:
|
||||
model_bundle, generation_data, sampling_preset = flake_data
|
||||
model, clip, vae = model_bundle
|
||||
positive_cond, negative_cond, latent, width, height, pos_text, neg_text = generation_data[:7]
|
||||
steps, cfg, sampler, scheduler = sampling_preset
|
||||
steps, cfg, sampler, scheduler, denoise = sampling_preset
|
||||
|
||||
# --- Filename prefix state ----------------------------------------------
|
||||
# Copy the incoming filename_state so we don't mutate the original tuple.
|
||||
@@ -653,7 +653,7 @@ class FlakeStack:
|
||||
|
||||
out_model_bundle = (model, clip, vae)
|
||||
out_generation_data = (positive_cond, negative_cond, latent, new_width, new_height, new_pos_text, new_neg_text, filename_state)
|
||||
out_sampling_preset = (steps, cfg, sampler, scheduler)
|
||||
out_sampling_preset = (steps, cfg, sampler, scheduler, denoise)
|
||||
|
||||
return (
|
||||
(out_model_bundle, out_generation_data, out_sampling_preset),
|
||||
|
||||
@@ -181,7 +181,7 @@ HALF = [KF(0.0, 1.0), KF(0.5, 0.0)]
|
||||
BASE_DATA = (
|
||||
("MODEL", "CLIP", "VAE"),
|
||||
([["P", {}]], [["N", {}]], {"samples": "l"}, 1024, 1024, "", "", {"stems": []}),
|
||||
(30, 4.0, "euler", "simple"),
|
||||
(30, 4.0, "euler", "simple", 1.0),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ def _run(model_family, *flake_cns, upstream_family=None):
|
||||
flake_data = (
|
||||
(FakeModel("BASE_MODEL"), "CLIP", "VAE"),
|
||||
([["P", {}]], [["N", {}]], {"samples": "l"}, 1024, 1024, "", "", {"stems": [], "model_family": upstream_family}),
|
||||
(30, 4.0, "euler", "simple"),
|
||||
(30, 4.0, "euler", "simple", 1.0),
|
||||
)
|
||||
entries = [{"name": n} for n in flakes]
|
||||
import json
|
||||
|
||||
@@ -464,7 +464,7 @@ def test_override_cfg_drives_the_decision_not_the_preset():
|
||||
the post-override value."""
|
||||
bundle = ("M", "C", "V")
|
||||
gen = ("POS", [["COND", {}]], "LAT", 1024, 1024, "p", "n", {"stems": []})
|
||||
samp = (30, 4.0, "euler", "simple") # preset says cfg 4
|
||||
samp = (30, 4.0, "euler", "simple", 1.0) # preset says cfg 4
|
||||
_b, _g, samp2 = full_flake_node._apply_preset_overrides(
|
||||
'{"cfg": 1.0}', bundle, gen, samp)
|
||||
assert samp2[1] == 1.0
|
||||
|
||||
Reference in New Issue
Block a user