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.
114 lines
4.1 KiB
Python
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,]
|