Files
DemonGatanjieu-Anomalous_Mo…/api/lora_info.py
T
DemonGatanjieuandClaude Opus 5.5 fa11c55293 feat(lora): one way to insert a LoRA, for Current node and MCP alike
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>
2026-10-05 23:00:32 +08:00

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)})