Current node's "Insert LoRA before / after" refused the most common workflows: a checkpoint whose CLIP feeds both prompts (one link per output only) and UNet workflows with their own CLIP loader (MODEL and CLIP had to come from one node). - graph_splice.js: planLoraInsertion decides the links (after a node: every link of its MODEL output; before one: only its own MODEL input) and, for LoRAs that train the text encoder, the CLIP line (same node, a UNet workflow's one CLIP source, or before a sampler the source its prompts share); spliceLora wires it, all or nothing. The old analyzeModelChainInsertion / spliceModelChainNode / spliceAfterOutputs are gone. - api/lora_info.py: GET /anomalous/lora_info reads the LoRA's safetensors header; the picker asks it on confirm and inserts a model-only loader when there are no text-encoder weights. The MCP add_lora uses the same helper. - Insertion messages speak of the MODEL line; the fan-out ones are removed. - tests/graph_splice.mjs (local) covers checkpoint, UNet + CLIP loader, before a sampler, and the rollback. Verified on 8199 through the Current node picker: after a checkpoint feeding two prompts (full LoRA, both prompts moved) and before a sampler (model-only Flux LoRA). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
41 lines
1.6 KiB
Python
41 lines
1.6 KiB
Python
"""What a LoRA file trains, read from its safetensors header: whether it also has text-encoder
|
|
weights, so inserting it wires the CLIP line too (graph_splice.js planLoraInsertion).
|
|
|
|
GET /anomalous/lora_info?name=<the loader's lora_name> -> {text_encoder}.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import struct
|
|
|
|
from aiohttp import web
|
|
import folder_paths
|
|
|
|
# Key prefixes of text-encoder weights: kohya lora_te / lora_te1 / lora_te2, te1 / te2, diffusers text_encoder.
|
|
TEXT_ENCODER_KEYS = ("lora_te", "te_", "te1", "te2", "text_encoder")
|
|
MAX_HEADER_BYTES = 100 * 1024 * 1024
|
|
|
|
|
|
def trains_text_encoder(path):
|
|
"""Whether the LoRA at `path` has text-encoder weights. Other formats, or a header that
|
|
cannot be read, count as yes: the CLIP line then goes through it, which costs nothing."""
|
|
if not str(path).lower().endswith(".safetensors"):
|
|
return True
|
|
try:
|
|
with open(path, "rb") as source:
|
|
size = struct.unpack("<Q", source.read(8))[0]
|
|
if size > MAX_HEADER_BYTES:
|
|
return True
|
|
keys = json.loads(source.read(size))
|
|
except (OSError, ValueError, struct.error):
|
|
return True
|
|
return any(key.startswith(TEXT_ENCODER_KEYS) for key in keys if key != "__metadata__")
|
|
|
|
|
|
async def api_lora_info(request):
|
|
name = request.query.get("name", "")
|
|
path = folder_paths.get_full_path("loras", name) if name else None # only names ComfyUI lists
|
|
if not path:
|
|
return web.json_response({"error": "Unknown LoRA"}, status=404)
|
|
return web.json_response({"text_encoder": await asyncio.to_thread(trains_text_encoder, path)})
|