Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e092c7777 | ||
|
|
140b32bcfb | ||
|
|
d81a3a2701 |
@@ -0,0 +1,33 @@
|
||||
name: Close inactive issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "17 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
close-inactive-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ github.token }}
|
||||
days-before-issue-stale: 23
|
||||
days-before-issue-close: 7
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: >
|
||||
This issue has had no activity for 23 days. It will be
|
||||
automatically closed in 7 days unless there is new activity.
|
||||
close-issue-message: >
|
||||
This issue was automatically closed after 30 days without
|
||||
activity. If the problem still exists, please reopen it or
|
||||
create a new issue with updated information.
|
||||
close-issue-reason: not_planned
|
||||
exempt-issue-labels: pinned,security,confirmed-bug
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" role="img" aria-labelledby="title desc">
|
||||
<title id="title">LoRA Pipeline icon</title>
|
||||
<desc id="desc">LP monogram in violet and yellow.</desc>
|
||||
<defs>
|
||||
<linearGradient id="panel" x1="42" y1="32" x2="360" y2="370" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#292632"/>
|
||||
<stop offset="1" stop-color="#17171d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="82" y1="94" x2="318" y2="304" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#b49aff"/>
|
||||
<stop offset="1" stop-color="#ffd400"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="400" height="400" rx="88" fill="#11131a"/>
|
||||
<rect x="28" y="28" width="344" height="344" rx="72" fill="url(#panel)" stroke="url(#accent)" stroke-width="10"/>
|
||||
<text x="200" y="252" fill="url(#accent)" font-family="Arial, Helvetica, sans-serif" font-size="150" font-weight="800" letter-spacing="-8" text-anchor="middle">LP</text>
|
||||
<circle cx="86" cy="314" r="9" fill="#b49aff"/>
|
||||
<circle cx="116" cy="314" r="9" fill="#ffd400"/>
|
||||
<path d="M146 314h168" stroke="url(#accent)" stroke-width="10" stroke-linecap="round" opacity=".55"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
+113
-25
@@ -1,3 +1,5 @@
|
||||
import math
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
@@ -78,6 +80,17 @@ class ConditioningPipelineCombine:
|
||||
},
|
||||
"optional": {
|
||||
"global_strength": ("FLOAT", {"default": 0.3, "min": 0.0, "max": 10.0, "step": 0.05}),
|
||||
"fast_mode": (
|
||||
"BOOLEAN",
|
||||
{
|
||||
"default": False,
|
||||
"tooltip": (
|
||||
"Faster full-canvas mode. Concatenates the global positive "
|
||||
"into every regional positive and avoids a separate global "
|
||||
"UNet pass. This changes image generation semantics."
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -100,7 +113,45 @@ class ConditioningPipelineCombine:
|
||||
mask[y_px : y_px + h_px, x_px : x_px + w_px] = 1.0
|
||||
return mask.unsqueeze(0)
|
||||
|
||||
def run(self, global_positive, global_negative, pipeline, global_strength=0.3):
|
||||
@staticmethod
|
||||
def _make_fast_mask(x, y, width, height, res):
|
||||
mask = torch.zeros(res, res)
|
||||
x_start = max(0, min(res, math.floor(x * res)))
|
||||
y_start = max(0, min(res, math.floor(y * res)))
|
||||
x_end = max(x_start, min(res, math.ceil((x + width) * res)))
|
||||
y_end = max(y_start, min(res, math.ceil((y + height) * res)))
|
||||
mask[y_start:y_end, x_start:x_end] = 1.0
|
||||
return mask.unsqueeze(0)
|
||||
|
||||
@staticmethod
|
||||
def _concat_global(conditioning, global_positive, global_strength):
|
||||
if global_strength <= 0.0 or not conditioning or not global_positive:
|
||||
return conditioning
|
||||
|
||||
global_tokens = global_positive[0][0] * global_strength
|
||||
combined = []
|
||||
for entry in conditioning:
|
||||
tokens = torch.cat((entry[0], global_tokens), dim=1)
|
||||
combined.append([tokens, entry[1].copy()])
|
||||
return combined
|
||||
|
||||
@staticmethod
|
||||
def _masks_cover_canvas(masks):
|
||||
if not masks:
|
||||
return False
|
||||
coverage = torch.zeros_like(masks[0], dtype=torch.bool)
|
||||
for mask in masks:
|
||||
coverage |= mask > 0
|
||||
return bool(torch.all(coverage).item())
|
||||
|
||||
def run(
|
||||
self,
|
||||
global_positive,
|
||||
global_negative,
|
||||
pipeline,
|
||||
global_strength=0.3,
|
||||
fast_mode=False,
|
||||
):
|
||||
from comfy_execution.graph_utils import GraphBuilder
|
||||
|
||||
global_strength = round(float(global_strength), 2)
|
||||
@@ -130,66 +181,103 @@ class ConditioningPipelineCombine:
|
||||
|
||||
graph = GraphBuilder()
|
||||
|
||||
# First entry: PairConditioningSetProperties (no previous combined state yet)
|
||||
if fast_mode:
|
||||
masks = []
|
||||
acc_pos = None
|
||||
|
||||
for item in valid:
|
||||
mask = self._make_fast_mask(
|
||||
item["x"], item["y"], item["width"], item["height"], self.BASE_RES,
|
||||
)
|
||||
masks.append(mask)
|
||||
regional_positive = self._concat_global(
|
||||
item["conditioning"], global_positive, global_strength,
|
||||
)
|
||||
|
||||
if acc_pos is None:
|
||||
acc = graph.node(
|
||||
"ConditioningSetProperties",
|
||||
cond_NEW=regional_positive,
|
||||
strength=item.get("strength", 1.0),
|
||||
set_cond_area="default",
|
||||
mask=mask,
|
||||
)
|
||||
else:
|
||||
acc = graph.node(
|
||||
"ConditioningSetPropertiesAndCombine",
|
||||
cond=acc_pos,
|
||||
cond_NEW=regional_positive,
|
||||
strength=item.get("strength", 1.0),
|
||||
set_cond_area="default",
|
||||
mask=mask,
|
||||
)
|
||||
acc_pos = acc.out(0)
|
||||
|
||||
if not self._masks_cover_canvas(masks):
|
||||
fallback = graph.node(
|
||||
"ConditioningSetDefaultCombine",
|
||||
cond=acc_pos,
|
||||
cond_DEFAULT=global_positive,
|
||||
)
|
||||
acc_pos = fallback.out(0)
|
||||
|
||||
return {
|
||||
"result": (acc_pos, global_negative, areas_list),
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
|
||||
# First entry: set properties on the regional positive conditioning.
|
||||
# The negative conditioning is global and identical for every region, so
|
||||
# evaluating masked copies of it would repeat the same UNet prediction.
|
||||
first = valid[0]
|
||||
mask_0 = self._make_mask(
|
||||
first["x"], first["y"], first["width"], first["height"], self.BASE_RES,
|
||||
)
|
||||
acc = graph.node(
|
||||
"PairConditioningSetProperties",
|
||||
positive_NEW=first["conditioning"],
|
||||
negative_NEW=global_negative,
|
||||
"ConditioningSetProperties",
|
||||
cond_NEW=first["conditioning"],
|
||||
strength=first.get("strength", 1.0),
|
||||
set_cond_area="default",
|
||||
mask=mask_0,
|
||||
)
|
||||
acc_pos = acc.out(0)
|
||||
acc_neg = acc.out(1)
|
||||
|
||||
# Entries siguientes: PairConditioningSetPropertiesAndCombine
|
||||
# Following entries: combine regional positive conditionings only.
|
||||
for item in valid[1:]:
|
||||
mask_i = self._make_mask(
|
||||
item["x"], item["y"], item["width"], item["height"], self.BASE_RES,
|
||||
)
|
||||
acc = graph.node(
|
||||
"PairConditioningSetPropertiesAndCombine",
|
||||
positive=acc_pos,
|
||||
negative=acc_neg,
|
||||
positive_NEW=item["conditioning"],
|
||||
negative_NEW=global_negative,
|
||||
"ConditioningSetPropertiesAndCombine",
|
||||
cond=acc_pos,
|
||||
cond_NEW=item["conditioning"],
|
||||
strength=item.get("strength", 1.0),
|
||||
set_cond_area="default",
|
||||
mask=mask_i,
|
||||
)
|
||||
acc_pos = acc.out(0)
|
||||
acc_neg = acc.out(1)
|
||||
|
||||
# Global como entrada full-image con strength controlable
|
||||
if global_strength > 0.0:
|
||||
global_mask = self._make_mask(0.0, 0.0, 1.0, 1.0, self.BASE_RES)
|
||||
acc = graph.node(
|
||||
"PairConditioningSetPropertiesAndCombine",
|
||||
positive=acc_pos,
|
||||
negative=acc_neg,
|
||||
positive_NEW=global_positive,
|
||||
negative_NEW=global_negative,
|
||||
"ConditioningSetPropertiesAndCombine",
|
||||
cond=acc_pos,
|
||||
cond_NEW=global_positive,
|
||||
strength=global_strength,
|
||||
set_cond_area="default",
|
||||
mask=global_mask,
|
||||
)
|
||||
acc_pos = acc.out(0)
|
||||
acc_neg = acc.out(1)
|
||||
|
||||
# Default fallback (safety net para pixeles sin cubrir)
|
||||
final = graph.node(
|
||||
"PairConditioningSetDefaultCombine",
|
||||
positive=acc_pos,
|
||||
negative=acc_neg,
|
||||
positive_DEFAULT=global_positive,
|
||||
negative_DEFAULT=global_negative,
|
||||
"ConditioningSetDefaultCombine",
|
||||
cond=acc_pos,
|
||||
cond_DEFAULT=global_positive,
|
||||
)
|
||||
|
||||
return {
|
||||
"result": (final.out(0), final.out(1), areas_list),
|
||||
"result": (final.out(0), global_negative, areas_list),
|
||||
"expand": graph.finalize(),
|
||||
}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "comfyui-lora-pipeline"
|
||||
version = "1.1.2"
|
||||
version = "1.1.4"
|
||||
description = "Area-based LoRA scheduling and area conditioning wrappers for ComfyUI."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -19,5 +19,5 @@ Documentation = "https://github.com/andreszs/comfyui-lora-pipeline#readme"
|
||||
[tool.comfy]
|
||||
PublisherId = "andreszs"
|
||||
DisplayName = "LoRA Pipeline"
|
||||
Icon = ""
|
||||
Icon = "https://raw.githubusercontent.com/andreszs/comfyui-lora-pipeline/main/assets/icon.svg"
|
||||
Banner = "https://raw.githubusercontent.com/andreszs/comfyui-lora-pipeline/refs/heads/main/assets/banner_112.gif"
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
_MODULE_PATH = Path(__file__).parents[1] / "nodes" / "cond_area_pipeline.py"
|
||||
_SPEC = importlib.util.spec_from_file_location("lora_pipeline_cond_area", _MODULE_PATH)
|
||||
_MODULE = importlib.util.module_from_spec(_SPEC)
|
||||
_SPEC.loader.exec_module(_MODULE)
|
||||
ConditioningPipelineCombine = _MODULE.ConditioningPipelineCombine
|
||||
|
||||
|
||||
class _Output:
|
||||
def __init__(self, node_type, index):
|
||||
self.node_type = node_type
|
||||
self.index = index
|
||||
|
||||
|
||||
class _Node:
|
||||
def __init__(self, node_type):
|
||||
self.node_type = node_type
|
||||
|
||||
def out(self, index):
|
||||
return _Output(self.node_type, index)
|
||||
|
||||
|
||||
class _GraphBuilder:
|
||||
instance = None
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
_GraphBuilder.instance = self
|
||||
|
||||
def node(self, node_type, **inputs):
|
||||
self.calls.append((node_type, inputs))
|
||||
return _Node(node_type)
|
||||
|
||||
def finalize(self):
|
||||
return self.calls
|
||||
|
||||
|
||||
class ConditioningPipelineCombineTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
graph_utils = types.ModuleType("comfy_execution.graph_utils")
|
||||
graph_utils.GraphBuilder = _GraphBuilder
|
||||
comfy_execution = types.ModuleType("comfy_execution")
|
||||
comfy_execution.graph_utils = graph_utils
|
||||
sys.modules["comfy_execution"] = comfy_execution
|
||||
sys.modules["comfy_execution.graph_utils"] = graph_utils
|
||||
|
||||
def test_negative_conditioning_is_not_duplicated_per_area(self):
|
||||
global_positive = object()
|
||||
global_negative = object()
|
||||
pipeline = [
|
||||
{
|
||||
"conditioning": object(),
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"width": 0.5,
|
||||
"height": 1.0,
|
||||
"strength": 0.8,
|
||||
},
|
||||
{
|
||||
"conditioning": object(),
|
||||
"x": 0.5,
|
||||
"y": 0.0,
|
||||
"width": 0.5,
|
||||
"height": 1.0,
|
||||
"strength": 0.8,
|
||||
},
|
||||
]
|
||||
|
||||
result = ConditioningPipelineCombine().run(
|
||||
global_positive,
|
||||
global_negative,
|
||||
pipeline,
|
||||
global_strength=0.3,
|
||||
)
|
||||
|
||||
positive, negative, areas = result["result"]
|
||||
calls = _GraphBuilder.instance.calls
|
||||
|
||||
self.assertIs(negative, global_negative)
|
||||
self.assertEqual(len(areas), 2)
|
||||
self.assertEqual(
|
||||
[node_type for node_type, _ in calls],
|
||||
[
|
||||
"ConditioningSetProperties",
|
||||
"ConditioningSetPropertiesAndCombine",
|
||||
"ConditioningSetPropertiesAndCombine",
|
||||
"ConditioningSetDefaultCombine",
|
||||
],
|
||||
)
|
||||
self.assertEqual(positive.node_type, "ConditioningSetDefaultCombine")
|
||||
self.assertFalse(any(node_type.startswith("PairConditioning") for node_type, _ in calls))
|
||||
|
||||
def test_fast_mode_concatenates_global_positive_and_skips_full_coverage_fallback(self):
|
||||
global_positive = [[torch.full((1, 1, 2), 2.0), {}]]
|
||||
global_negative = object()
|
||||
pipeline = [
|
||||
{
|
||||
"conditioning": [[torch.full((1, 1, 2), 1.0), {}]],
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"width": 0.5,
|
||||
"height": 1.0,
|
||||
"strength": 0.8,
|
||||
},
|
||||
{
|
||||
"conditioning": [[torch.full((1, 1, 2), 3.0), {}]],
|
||||
"x": 0.5,
|
||||
"y": 0.0,
|
||||
"width": 0.5,
|
||||
"height": 1.0,
|
||||
"strength": 0.8,
|
||||
},
|
||||
]
|
||||
|
||||
result = ConditioningPipelineCombine().run(
|
||||
global_positive,
|
||||
global_negative,
|
||||
pipeline,
|
||||
global_strength=0.25,
|
||||
fast_mode=True,
|
||||
)
|
||||
|
||||
positive, negative, _ = result["result"]
|
||||
calls = _GraphBuilder.instance.calls
|
||||
first_tokens = calls[0][1]["cond_NEW"][0][0]
|
||||
|
||||
self.assertIs(negative, global_negative)
|
||||
self.assertEqual(
|
||||
[node_type for node_type, _ in calls],
|
||||
[
|
||||
"ConditioningSetProperties",
|
||||
"ConditioningSetPropertiesAndCombine",
|
||||
],
|
||||
)
|
||||
self.assertEqual(positive.node_type, "ConditioningSetPropertiesAndCombine")
|
||||
self.assertEqual(first_tokens.shape[1], 2)
|
||||
torch.testing.assert_close(first_tokens[:, 1:, :], torch.full((1, 1, 2), 0.5))
|
||||
|
||||
def test_fast_mode_keeps_global_fallback_for_incomplete_coverage(self):
|
||||
global_positive = [[torch.ones((1, 1, 2)), {}]]
|
||||
pipeline = [
|
||||
{
|
||||
"conditioning": [[torch.ones((1, 1, 2)), {}]],
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"width": 0.5,
|
||||
"height": 1.0,
|
||||
"strength": 1.0,
|
||||
},
|
||||
]
|
||||
|
||||
result = ConditioningPipelineCombine().run(
|
||||
global_positive,
|
||||
object(),
|
||||
pipeline,
|
||||
fast_mode=True,
|
||||
)
|
||||
|
||||
positive = result["result"][0]
|
||||
calls = _GraphBuilder.instance.calls
|
||||
|
||||
self.assertEqual(calls[-1][0], "ConditioningSetDefaultCombine")
|
||||
self.assertEqual(positive.node_type, "ConditioningSetDefaultCombine")
|
||||
|
||||
def test_fast_mode_overlapping_fractional_regions_cover_canvas(self):
|
||||
conditioning = [[torch.ones((1, 1, 2)), {}]]
|
||||
pipeline = [
|
||||
{
|
||||
"conditioning": conditioning,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"width": 0.52,
|
||||
"height": 1.0,
|
||||
"strength": 1.0,
|
||||
},
|
||||
{
|
||||
"conditioning": conditioning,
|
||||
"x": 0.48,
|
||||
"y": 0.0,
|
||||
"width": 0.52,
|
||||
"height": 1.0,
|
||||
"strength": 1.0,
|
||||
},
|
||||
]
|
||||
|
||||
ConditioningPipelineCombine().run(
|
||||
conditioning,
|
||||
object(),
|
||||
pipeline,
|
||||
fast_mode=True,
|
||||
)
|
||||
|
||||
calls = _GraphBuilder.instance.calls
|
||||
self.assertFalse(
|
||||
any(node_type == "ConditioningSetDefaultCombine" for node_type, _ in calls),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user