Files
marcoc2-ComfyUI-AnotherUtils/loaders/utils.py
T
Marco 793402f93f feat: add LTXV Multi Concat nodes and metadata extraction image loaders
This commit introduces several new performance-oriented custom nodes and utilities:

- Added LTXV Multi Concat and LTXV Multi Concat (beta) for faster frame injection using latent inpainting logic.

- Refactored LTXV index resolution logic into ltxv_utils.py.

- Updated ImageListSampler to anchor edge frames properly and support output index normalization scaling via target_frames.

- Added metadata-based prompt extraction image loaders (LoadImageAndExtractPrompt, FolderImageAndExtractPrompt, and FolderImageMetadataByName).

- Included animation tracking and camera switcher nodes.
2026-04-15 15:55:48 -03:00

114 lines
4.1 KiB
Python

import json
import torch
import numpy as np
from PIL import Image, ImageOps
class PromptExtractor:
@staticmethod
def extract_from_image(img):
"""
Extracts the prompt string from PIL Image metadata.
Returns a string.
"""
metadata = img.info
if not metadata:
return ""
# 1. ComfyUI Standard
if 'prompt' in metadata:
try:
prompt_json = json.loads(metadata['prompt'])
return PromptExtractor.extract_from_json(prompt_json)
except Exception as e:
print(f"[AnotherUtils] Error parsing prompt JSON: {e}")
# 2. A1111 / WebUI Standard
if 'parameters' in metadata:
return metadata['parameters'].split("\n")[0]
return ""
@staticmethod
def extract_from_json(prompt_json):
"""
Heuristic to find the 'main' positive prompt in a ComfyUI graph.
"""
candidates = []
# Find all sampler-like nodes
sampler_types = ['Sampler', 'Guider', 'SamplerCustom', 'KSampler', 'Flux2Sampler']
sampler_nodes = {id: node for id, node in prompt_json.items()
if any(st in node.get('class_type', '') for st in sampler_types)}
for sid, snode in sampler_nodes.items():
# Follow 'positive' link
inputs = snode.get('inputs', {})
pos = inputs.get('positive') or inputs.get('cond') or inputs.get('conditioning')
if isinstance(pos, list):
text = PromptExtractor._trace_back(prompt_json, str(pos[0]))
if text:
# Give higher score if it's connected to a sampler
candidates.append((text, 10))
# Fallback: Look for all CLIPTextEncode nodes
for id, node in prompt_json.items():
ctype = node.get('class_type', '')
if 'CLIPTextEncode' in ctype:
inputs = node.get('inputs', {})
# Handle SDXL (text_g, text_l)
text = inputs.get('text') or inputs.get('text_g') or inputs.get('text_l')
if text and isinstance(text, str):
score = 5
title = node.get('_meta', {}).get('title', '').lower()
if 'positive' in title:
score += 5
candidates.append((text, score))
if not candidates:
# Last ditch: look for any string longer than 20 chars in inputs
for id, node in prompt_json.items():
for val in node.get('inputs', {}).values():
if isinstance(val, str) and len(val) > 20:
candidates.append((val, 1))
if candidates:
# Sort by score (desc), then by length (desc)
candidates.sort(key=lambda x: (x[1], len(x[0])), reverse=True)
return candidates[0][0]
return "Prompt not found"
@staticmethod
def _trace_back(prompt_json, node_id, visited=None):
if visited is None:
visited = set()
if node_id in visited or node_id not in prompt_json:
return None
visited.add(node_id)
node = prompt_json[node_id]
inputs = node.get('inputs', {})
# Direct text hit
text = inputs.get('text') or inputs.get('text_g') or inputs.get('text_l')
if text and isinstance(text, str) and len(text.strip()) > 0:
return text
# Recursive trace for conditioning links
for key in ['conditioning', 'cond', 'text', 'string', 'samples']:
val = inputs.get(key)
if isinstance(val, list):
result = PromptExtractor._trace_back(prompt_json, str(val[0]), visited)
if result:
return result
return None
@staticmethod
def preprocess_image(img):
"""Shared image to tensor logic"""
img = ImageOps.exif_transpose(img)
image = img.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
return torch.from_numpy(image)[None,]