修改局部重绘的BUG

This commit is contained in:
Gin3601
2026-05-21 14:12:44 +08:00
parent b9ddd45f04
commit 97e12a83f4
7 changed files with 1751 additions and 6 deletions
+1 -1
View File
@@ -145,7 +145,7 @@ class LLSSimpleVAEEncode:
编码本身复用 ComfyUI 原生 vae.encode,只在外层补尺寸处理与元信息。
"""
CATEGORY = "LLS-node"
CATEGORY = "LLS/Image"
FUNCTION = "encode"
RETURN_TYPES = ("LATENT", "INT", "INT", "STRING")
RETURN_NAMES = ("latent", "width", "height", "latent_info")
+44 -2
View File
@@ -116,6 +116,42 @@ def _normalize_flux_guidance(value, fallback: float | None) -> float | None:
return normalized
def _normalize_ksampler_compat_values(
denoise_mode,
adapter_mode,
flux_guidance,
model_family,
):
normalized_denoise_mode = str(denoise_mode or "").strip()
normalized_flux_guidance = str(flux_guidance or "").strip()
normalized_adapter_mode = str(adapter_mode or "").strip()
normalized_model_family = str(model_family or "").strip()
# Repair the short-lived widget-order regression so old and newly re-saved
# workflows can both be interpreted correctly.
if (
normalized_denoise_mode not in _DENOISE_MODE_CHOICES
and normalized_flux_guidance in _DENOISE_MODE_CHOICES
and normalized_model_family.lower() in _ADAPTER_MODE_CHOICES
):
shifted_flux_guidance = denoise_mode
denoise_mode = normalized_flux_guidance or "manual"
adapter_mode = normalized_model_family.lower() or "auto"
flux_guidance = shifted_flux_guidance
model_family = "Auto" if normalized_adapter_mode.lower() == "auto" else normalized_adapter_mode
return denoise_mode, adapter_mode, flux_guidance, model_family
if normalized_adapter_mode.lower() in _ADAPTER_MODE_CHOICES:
adapter_mode = normalized_adapter_mode.lower()
elif normalized_adapter_mode == "Auto":
adapter_mode = "auto"
if normalized_model_family == "auto":
model_family = "Auto"
return denoise_mode, adapter_mode, flux_guidance, model_family
def _common_ksampler(
model,
seed: int,
@@ -240,13 +276,13 @@ class LLSSimpleKSampler:
"sampler_name": (_get_samplers(), {"default": "euler_ancestral"}),
"scheduler": (_get_schedulers(), {"default": "karras"}),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"denoise_mode": (_DENOISE_MODE_CHOICES, {"default": "manual"}),
"adapter_mode": (_ADAPTER_MODE_CHOICES, {"default": "auto"}),
"flux_guidance": (
_PRIMITIVE_NUMBER_INPUT,
{"default": 3.5, "widgetType": "FLOAT", "min": 0.0, "max": 100.0, "step": 0.1, "round": 0.1},
),
"model_family": (MODEL_FAMILY_CHOICES, {"default": "Auto"}),
"denoise_mode": (_DENOISE_MODE_CHOICES, {"default": "manual"}),
"adapter_mode": (_ADAPTER_MODE_CHOICES, {"default": "auto"}),
},
"optional": {
"repair_info": ("LLS_REPAIR_INFO",),
@@ -302,6 +338,12 @@ class LLSSimpleKSampler:
repair_meta = normalize_repair_info(repair_info) if repair_info is not None else None
model_meta = normalize_model_info(model_info)
denoise_mode, adapter_mode, flux_guidance, model_family = _normalize_ksampler_compat_values(
denoise_mode,
adapter_mode,
flux_guidance,
model_family,
)
family = resolve_model_family(model_family, model=model)
if repair_meta is not None and repair_meta.get("model_family") not in {"UNKNOWN", "", None}:
+4 -3
View File
@@ -593,7 +593,7 @@ class TestLoaderPromptRefactor(unittest.TestCase):
self.assertEqual(sample_data["sampler_name"], "euler")
self.assertEqual(latent["samples"].shape, (1, 4, 64, 64))
def test_ksampler_schema_appends_repair_widgets_after_legacy_widgets(self):
def test_ksampler_schema_keeps_legacy_widget_order_for_workflow_compatibility(self):
plugin = load_plugin_package()
node_cls = plugin.NODE_CLASS_MAPPINGS["LLSSimpleKSampler"]
required = node_cls.INPUT_TYPES()["required"]
@@ -612,10 +612,10 @@ class TestLoaderPromptRefactor(unittest.TestCase):
"sampler_name",
"scheduler",
"denoise",
"flux_guidance",
"model_family",
"denoise_mode",
"adapter_mode",
"flux_guidance",
"model_family",
),
)
self.assertEqual(required["model_family"][1]["default"], "Auto")
@@ -700,6 +700,7 @@ class TestLoaderPromptRefactor(unittest.TestCase):
required = node_cls.INPUT_TYPES()["required"]
optional = node_cls.INPUT_TYPES()["optional"]
self.assertEqual(node_cls.CATEGORY, "LLS/Image")
self.assertEqual(node_cls.RETURN_TYPES, ("LATENT", "INT", "INT", "STRING"))
self.assertEqual(required["image"][0], "IMAGE")
self.assertEqual(required["vae"][0], "VAE")
+48
View File
@@ -112,6 +112,54 @@ class TestRepairSampler(unittest.TestCase):
self.assertEqual(payload["repair_scope"], "region")
self.assertEqual(payload["repair_kernel"], "latent_mask")
def test_sampler_recovers_shifted_values_from_broken_widget_order(self):
load_plugin_package()
from lls_node_test_repair.sampling import nodes as sampling_nodes
node = sampling_nodes.LLSSimpleKSampler()
latent = {"samples": FakeLatentTensor((1, 4, 64, 64)), "source": "repair_prepare_region"}
with mock.patch.object(sampling_nodes, "comfy_sample", object()), mock.patch.object(
sampling_nodes,
"comfy_samplers",
types.SimpleNamespace(KSampler=types.SimpleNamespace(SAMPLERS=["euler"], SCHEDULERS=["karras"])),
), mock.patch.object(
sampling_nodes,
"_common_ksampler",
return_value={"samples": "done"},
) as common:
_result_latent, sample_info = node.sample(
model=types.SimpleNamespace(_lls_family="FLUX_DEV"),
positive="positive",
negative="negative",
latent_image=latent,
quality_preset="Manual",
seed=7,
steps=20,
cfg=7.0,
sampler_name="euler",
scheduler="karras",
denoise=0.33,
denoise_mode=4.2,
adapter_mode="Auto",
flux_guidance="auto_from_repair",
model_family="auto",
repair_info={
"repair_scope": "region",
"repair_kernel": "latent_mask",
"recommended_denoise": 0.61,
"model_family": "FLUX_DEV",
"model_role": "normal",
},
guidance_stack=None,
model_info=None,
)
self.assertEqual(common.call_args.kwargs["denoise"], 0.61)
payload = json.loads(sample_info)
self.assertEqual(payload["family"], "FLUX_DEV")
self.assertEqual(payload["guidance"], 4.2)
self.assertTrue(payload["repair_mode"])
def test_sampler_rejects_unsupported_qwen_adapter(self):
load_plugin_package()
from lls_node_test_repair.sampling import nodes as sampling_nodes
+26
View File
@@ -0,0 +1,26 @@
import unittest
try:
from .test_mask_draw_helpers import ROOT
except ImportError:
from test_mask_draw_helpers import ROOT
class TestSamplingFrontendCompat(unittest.TestCase):
def test_ksampler_compat_asset_exists(self):
asset = ROOT / "web" / "js" / "lls_ksampler_compat.js"
self.assertTrue(asset.exists(), msg=f"Missing frontend asset: {asset}")
def test_ksampler_compat_asset_targets_sampler_node(self):
asset = (ROOT / "web" / "js" / "lls_ksampler_compat.js").read_text(encoding="utf-8")
self.assertIn("app.registerExtension", asset)
self.assertIn("LLSSimpleKSampler", asset)
self.assertIn("LLS Simple KSampler", asset)
self.assertIn("widgets_values", asset)
self.assertIn("configure", asset)
if __name__ == "__main__":
unittest.main()
+97
View File
@@ -0,0 +1,97 @@
import { app } from "../../../scripts/app.js";
const EXTENSION_NAME = "lls.ksampler.compat";
const TARGET_NODE_CLASS = "LLSSimpleKSampler";
const TARGET_NODE_DISPLAY_NAME = "LLS Simple KSampler";
const DENOISE_MODE_INDEX = 11;
const ADAPTER_MODE_INDEX = 12;
const FLUX_GUIDANCE_INDEX = 13;
const MODEL_FAMILY_INDEX = 14;
const DENOISE_MODES = new Set(["manual", "auto_from_repair"]);
const ADAPTER_MODES = new Set(["auto", "sd_classic", "flux", "sd3", "qwen", "zimage"]);
function normalizeText(value) {
if (typeof value === "string") {
return value.trim();
}
if (value === null || value === undefined) {
return "";
}
return String(value).trim();
}
function isNumericLike(value) {
if (typeof value === "number") {
return Number.isFinite(value);
}
const text = normalizeText(value);
if (!text) {
return false;
}
return Number.isFinite(Number(text));
}
function looksLikeShiftedWidgetOrder(values) {
if (!Array.isArray(values) || values.length <= MODEL_FAMILY_INDEX) {
return false;
}
const denoiseMode = normalizeText(values[DENOISE_MODE_INDEX]);
const adapterMode = normalizeText(values[ADAPTER_MODE_INDEX]).toLowerCase();
const fluxGuidance = normalizeText(values[FLUX_GUIDANCE_INDEX]);
const modelFamily = normalizeText(values[MODEL_FAMILY_INDEX]).toLowerCase();
return (
!DENOISE_MODES.has(denoiseMode) &&
DENOISE_MODES.has(fluxGuidance) &&
ADAPTER_MODES.has(modelFamily) &&
!ADAPTER_MODES.has(adapterMode) &&
(isNumericLike(values[DENOISE_MODE_INDEX]) || denoiseMode === "")
);
}
function migrateWidgetValues(values) {
if (!Array.isArray(values) || values.length <= MODEL_FAMILY_INDEX) {
return values;
}
const migrated = values.slice();
if (looksLikeShiftedWidgetOrder(migrated)) {
const shiftedDenoiseMode = migrated[DENOISE_MODE_INDEX];
const shiftedAdapterMode = migrated[ADAPTER_MODE_INDEX];
const shiftedFluxGuidance = migrated[FLUX_GUIDANCE_INDEX];
const shiftedModelFamily = migrated[MODEL_FAMILY_INDEX];
migrated[DENOISE_MODE_INDEX] = normalizeText(shiftedFluxGuidance) || "manual";
migrated[ADAPTER_MODE_INDEX] = normalizeText(shiftedModelFamily).toLowerCase() || "auto";
migrated[FLUX_GUIDANCE_INDEX] = shiftedDenoiseMode;
migrated[MODEL_FAMILY_INDEX] =
normalizeText(shiftedAdapterMode).toLowerCase() === "auto" ? "Auto" : shiftedAdapterMode;
return migrated;
}
if (normalizeText(migrated[ADAPTER_MODE_INDEX]) === "Auto") {
migrated[ADAPTER_MODE_INDEX] = "auto";
}
if (normalizeText(migrated[MODEL_FAMILY_INDEX]) === "auto") {
migrated[MODEL_FAMILY_INDEX] = "Auto";
}
return migrated;
}
app.registerExtension({
name: EXTENSION_NAME,
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name !== TARGET_NODE_CLASS && nodeData.display_name !== TARGET_NODE_DISPLAY_NAME) {
return;
}
const previousConfigure = nodeType.prototype.configure;
nodeType.prototype.configure = function configureLLSKSamplerCompat(info) {
if (info && Array.isArray(info.widgets_values)) {
info.widgets_values = migrateWidgetValues(info.widgets_values);
}
return previousConfigure?.apply(this, arguments);
};
},
});
+1531
View File
File diff suppressed because it is too large Load Diff