✨ (CivitMetadataHelper): introduce CivitAI metadata helper node to collect and format metadata for submissions

📝 (init.py): add import statements for CivitMetadataHelper node and handle import errors
♻️ (swiss-army-knife.js): update node registration to include CivitMetadataHelper and add metadata preview functionality
⬆️ (pyproject.toml): bump version to 2.5.8 to reflect new features and updates
This commit is contained in:
Sam Kumar
2025-10-09 07:19:45 -05:00
parent f5c80166bb
commit 30367e35c1
4 changed files with 389 additions and 2 deletions
+12 -1
View File
@@ -13,6 +13,15 @@ except ImportError as e:
CONTROL_PANEL_NODE_CLASS_MAPPINGS = {}
CONTROL_PANEL_NODE_DISPLAY_NAME_MAPPINGS = {}
# Import civit metadata helper node
try:
from .nodes.utils.civit_metadata_helper import CIVIT_METADATA_HELPER_NODE_CLASS_MAPPINGS
from .nodes.utils.civit_metadata_helper import CIVIT_METADATA_HELPER_NODE_DISPLAY_NAME_MAPPINGS
except ImportError as e:
print(f"Warning: Could not import civit metadata helper node: {e}")
CIVIT_METADATA_HELPER_NODE_CLASS_MAPPINGS = {}
CIVIT_METADATA_HELPER_NODE_DISPLAY_NAME_MAPPINGS = {}
# Import lora_manager nodes
try:
from .nodes.lora_manager import NODE_CLASS_MAPPINGS as LORA_MANAGER_NODE_CLASS_MAPPINGS
@@ -68,11 +77,12 @@ def get_version():
import time
return str(int(time.time()))
# Combine main nodes, helper nodes, control panel, lora_manager, and media_selection nodes
# Combine main nodes, helper nodes, control panel, civit metadata helper, lora_manager, and media_selection nodes
NODE_CLASS_MAPPINGS = {
**MAIN_NODE_CLASS_MAPPINGS,
**HELPER_NODE_CLASS_MAPPINGS,
**CONTROL_PANEL_NODE_CLASS_MAPPINGS,
**CIVIT_METADATA_HELPER_NODE_CLASS_MAPPINGS,
**LORA_MANAGER_NODE_CLASS_MAPPINGS,
**MEDIA_SELECTION_NODE_CLASS_MAPPINGS
}
@@ -80,6 +90,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
**MAIN_NODE_DISPLAY_NAME_MAPPINGS,
**HELPER_NODE_DISPLAY_NAME_MAPPINGS,
**CONTROL_PANEL_NODE_DISPLAY_NAME_MAPPINGS,
**CIVIT_METADATA_HELPER_NODE_DISPLAY_NAME_MAPPINGS,
**LORA_MANAGER_NODE_DISPLAY_NAME_MAPPINGS,
**MEDIA_SELECTION_NODE_DISPLAY_NAME_MAPPINGS
}
+254
View File
@@ -0,0 +1,254 @@
"""
CivitAI Metadata Helper Node - Collects and formats metadata for CivitAI submissions
"""
import json
from typing import Dict, Any
class CivitMetadataHelper:
"""
A helper node that collects common generation parameters used for CivitAI model submissions.
This node aggregates sampling parameters and prompts into a structured format.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
"""
Define input fields for common CivitAI metadata parameters.
"""
return {
"required": {
"steps": ("INT", {
"default": 20,
"min": 1,
"max": 200,
"step": 1,
"tooltip": "Number of sampling steps"
}),
"cfg": ("FLOAT", {
"default": 7.0,
"min": 0.0,
"max": 30.0,
"step": 0.1,
"tooltip": "CFG Scale (Classifier Free Guidance)"
}),
"high_sampler": ("STRING", {
"default": "euler",
"tooltip": "High resolution sampler name"
}),
"low_sampler": ("STRING", {
"default": "euler",
"tooltip": "Low resolution sampler name"
}),
"lora_high": ("STRING", {
"default": "",
"tooltip": "High resolution LoRA model name"
}),
"lora_low": ("STRING", {
"default": "",
"tooltip": "Low resolution LoRA model name"
}),
"positive_prompt": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "Positive prompt text"
}),
"negative_prompt": ("STRING", {
"default": "",
"multiline": True,
"tooltip": "Negative prompt text"
})
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("formatted_metadata", "json_metadata", "summary")
FUNCTION = "collect_metadata"
CATEGORY = "Swiss Army Knife 🔪/Utils"
OUTPUT_NODE = True # Makes this an output node that displays in the UI
def collect_metadata(self, steps, cfg, high_sampler, low_sampler, lora_high, lora_low, positive_prompt, negative_prompt):
"""
Collect and format metadata for CivitAI submissions.
Args:
steps: Number of sampling steps
cfg: CFG scale value
high_sampler: High resolution sampler name
low_sampler: Low resolution sampler name
lora_high: High resolution LoRA model name
lora_low: Low resolution LoRA model name
positive_prompt: Positive prompt text
negative_prompt: Negative prompt text
Returns:
Tuple of (formatted_metadata, json_metadata, summary)
"""
# Create structured metadata
metadata = {
"generation_parameters": {
"steps": steps,
"cfg_scale": cfg,
"samplers": {
"high_res": high_sampler,
"low_res": low_sampler
},
"loras": {
"high_res": lora_high.strip(),
"low_res": lora_low.strip()
}
},
"prompts": {
"positive": positive_prompt.strip(),
"negative": negative_prompt.strip()
},
"prompt_lengths": {
"positive_chars": len(positive_prompt.strip()),
"negative_chars": len(negative_prompt.strip())
}
}
# Create formatted text version for display
formatted_metadata = self._format_for_display(metadata)
# Create JSON version for data processing
json_metadata = json.dumps(metadata, indent=2)
# Create summary for quick reference
lora_info = ""
if lora_high.strip() or lora_low.strip():
lora_info = f" | LoRAs: {lora_high.strip() or '(none)'}/{lora_low.strip() or '(none)'}"
summary = (
f"Steps: {steps} | CFG: {cfg} | "
f"Samplers: {high_sampler}/{low_sampler}{lora_info} | "
f"Pos: {len(positive_prompt.strip())} chars | "
f"Neg: {len(negative_prompt.strip())} chars"
)
# Log summary to console
print("\n" + "="*60)
print("CIVITAI METADATA HELPER - Generation Parameters")
print("="*60)
print(f"📊 Steps: {steps}")
print(f"📊 CFG Scale: {cfg}")
print(f"📊 High Sampler: {high_sampler}")
print(f"📊 Low Sampler: {low_sampler}")
print(f"📊 High LoRA: {lora_high.strip() or '(none)'}")
print(f"📊 Low LoRA: {lora_low.strip() or '(none)'}")
print(f"📊 Positive Prompt ({len(positive_prompt.strip())} chars):")
if positive_prompt.strip():
print(f" {positive_prompt.strip()[:200]}...")
else:
print(" (empty)")
print(f"📊 Negative Prompt ({len(negative_prompt.strip())} chars):")
if negative_prompt.strip():
print(f" {negative_prompt.strip()[:200]}...")
else:
print(" (empty)")
print("="*60 + "\n")
return (formatted_metadata, json_metadata, summary)
def _format_for_display(self, metadata: Dict[str, Any]) -> str:
"""
Format metadata for human-readable display.
Args:
metadata: Structured metadata dictionary
Returns:
Formatted string for display
"""
lines = []
# Header
lines.append("=" * 50)
lines.append("🎯 CIVITAI METADATA HELPER")
lines.append("=" * 50)
# Generation Parameters
lines.append("\n📊 GENERATION PARAMETERS")
lines.append("-" * 30)
gen_params = metadata["generation_parameters"]
lines.append(f"Steps: {gen_params['steps']}")
lines.append(f"CFG Scale: {gen_params['cfg_scale']}")
lines.append(f"High Sampler: {gen_params['samplers']['high_res']}")
lines.append(f"Low Sampler: {gen_params['samplers']['low_res']}")
lines.append(f"High LoRA: {gen_params['loras']['high_res'] or '(none)'}")
lines.append(f"Low LoRA: {gen_params['loras']['low_res'] or '(none)'}")
# Prompts
lines.append("\n📝 PROMPTS")
lines.append("-" * 30)
prompts = metadata["prompts"]
prompt_lengths = metadata["prompt_lengths"]
lines.append(f"Positive ({prompt_lengths['positive_chars']} chars):")
if prompts["positive"]:
# Split long prompts into multiple lines
pos_lines = self._wrap_text(prompts["positive"], 60)
for line in pos_lines:
lines.append(f" {line}")
else:
lines.append(" (empty)")
lines.append(f"\nNegative ({prompt_lengths['negative_chars']} chars):")
if prompts["negative"]:
# Split long prompts into multiple lines
neg_lines = self._wrap_text(prompts["negative"], 60)
for line in neg_lines:
lines.append(f" {line}")
else:
lines.append(" (empty)")
lines.append("\n" + "=" * 50)
return "\n".join(lines)
def _wrap_text(self, text: str, width: int) -> list:
"""
Wrap text to specified width while preserving words.
Args:
text: Text to wrap
width: Maximum line width
Returns:
List of wrapped lines
"""
words = text.split()
lines = []
current_line = []
current_length = 0
for word in words:
if current_length + len(word) + 1 <= width:
current_line.append(word)
current_length += len(word) + 1
else:
if current_line:
lines.append(" ".join(current_line))
current_line = [word]
current_length = len(word)
if current_line:
lines.append(" ".join(current_line))
return lines
# Export node class mapping
CIVIT_METADATA_HELPER_NODE_CLASS_MAPPINGS = {
"CivitMetadataHelper": CivitMetadataHelper
}
# Export display name mapping
CIVIT_METADATA_HELPER_NODE_DISPLAY_NAME_MAPPINGS = {
"CivitMetadataHelper": "🏷️ CivitAI Metadata Helper"
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "ComfyUI-SwissArmyKnife"
version = "2.5.7"
version = "2.5.8"
description = "A collection of custom nodes for ComfyUI"
authors = [
{name = "Sam Kumar", email = "sam@skproductions.llc"}
+122
View File
@@ -1096,6 +1096,128 @@ app.registerExtension({
return result;
};
}
// Handle CivitMetadataHelper node
else if (nodeData.name === "CivitMetadataHelper") {
debugLog("Registering CivitMetadataHelper node");
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
// Add a formatted display widget for metadata preview
this.addWidget(
"text",
"metadata_preview",
"🏷️ Metadata preview will appear here...",
(value) => {
// Read-only preview widget
},
{
multiline: true,
serialize: false, // Don't store in workflow JSON
}
);
// Function to update the metadata preview
this.updateMetadataPreview = function () {
try {
// Get input values
const steps = this.widgets.find((w) => w.name === "steps")?.value || 20;
const cfg = this.widgets.find((w) => w.name === "cfg")?.value || 7.0;
const highSampler =
this.widgets.find((w) => w.name === "high_sampler")?.value || "";
const lowSampler =
this.widgets.find((w) => w.name === "low_sampler")?.value || "";
const loraHigh =
this.widgets.find((w) => w.name === "lora_high")?.value || "";
const loraLow =
this.widgets.find((w) => w.name === "lora_low")?.value || "";
const positivePrompt =
this.widgets.find((w) => w.name === "positive_prompt")?.value || "";
const negativePrompt =
this.widgets.find((w) => w.name === "negative_prompt")?.value || "";
// Create preview text
let preview = "🏷️ CIVITAI METADATA PREVIEW\n";
preview += "========================================\n\n";
preview += `📊 Steps: ${steps}\n`;
preview += `📊 CFG Scale: ${cfg}\n`;
preview += `📊 High Sampler: ${highSampler || "(not set)"}\n`;
preview += `📊 Low Sampler: ${lowSampler || "(not set)"}\n`;
preview += `📊 High LoRA: ${loraHigh || "(not set)"}\n`;
preview += `📊 Low LoRA: ${loraLow || "(not set)"}\n\n`;
preview += `📝 Positive Prompt (${positivePrompt.length} chars):\n`;
preview += positivePrompt
? `${positivePrompt.substring(0, 100)}${positivePrompt.length > 100 ? "..." : ""}\n\n`
: "(empty)\n\n";
preview += `📝 Negative Prompt (${negativePrompt.length} chars):\n`;
preview += negativePrompt
? `${negativePrompt.substring(0, 100)}${negativePrompt.length > 100 ? "..." : ""}`
: "(empty)";
// Update the preview widget
const previewWidget = this.widgets.find(
(w) => w.name === "metadata_preview"
);
if (previewWidget) {
previewWidget.value = preview;
}
} catch (error) {
debugLog("Error updating metadata preview:", error);
}
};
// Set up listeners for input changes
this.widgets.forEach((widget) => {
if (
[
"steps",
"cfg",
"high_sampler",
"low_sampler",
"lora_high",
"lora_low",
"positive_prompt",
"negative_prompt",
].includes(widget.name)
) {
const originalCallback = widget.callback;
widget.callback = (value) => {
if (originalCallback) originalCallback(value);
this.updateMetadataPreview();
};
}
});
// Initial preview update
setTimeout(() => {
this.updateMetadataPreview();
}, 100);
return result;
};
// Handle execution results to update the display
const origOnExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
debugLog("[CivitMetadataHelper] onExecuted called with:", message);
if (origOnExecuted) {
origOnExecuted.call(this, message);
}
// Update preview if we have new output
if (message?.output) {
// The first output is the formatted metadata
const formattedMetadata = message.output[0];
const previewWidget = this.widgets.find((w) => w.name === "metadata_preview");
if (previewWidget && formattedMetadata) {
previewWidget.value = formattedMetadata;
}
}
};
}
},
// Hook to handle workflow loading