feat: Flake Inpaint chain node feeding Flake Generate (#428)

This commit is contained in:
Mehdi Chaid
2026-10-21 13:19:10 -04:00
parent c5f335ed80
commit dfcea9d757
7 changed files with 88 additions and 52 deletions
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
+1 -1
View File
@@ -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),
)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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