From 30367e35c138d01500786a53a00a32cc80bdba73 Mon Sep 17 00:00:00 2001 From: Sam Kumar Date: Thu, 9 Oct 2025 07:19:45 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(CivitMetadataHelper):=20introduce?= =?UTF-8?q?=20CivitAI=20metadata=20helper=20node=20to=20collect=20and=20fo?= =?UTF-8?q?rmat=20metadata=20for=20submissions=20=F0=9F=93=9D=20(init.py):?= =?UTF-8?q?=20add=20import=20statements=20for=20CivitMetadataHelper=20node?= =?UTF-8?q?=20and=20handle=20import=20errors=20=E2=99=BB=EF=B8=8F=20(swiss?= =?UTF-8?q?-army-knife.js):=20update=20node=20registration=20to=20include?= =?UTF-8?q?=20CivitMetadataHelper=20and=20add=20metadata=20preview=20funct?= =?UTF-8?q?ionality=20=E2=AC=86=EF=B8=8F=20(pyproject.toml):=20bump=20vers?= =?UTF-8?q?ion=20to=202.5.8=20to=20reflect=20new=20features=20and=20update?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 13 +- nodes/utils/civit_metadata_helper.py | 254 +++++++++++++++++++++++++++ pyproject.toml | 2 +- web/js/swiss-army-knife.js | 122 +++++++++++++ 4 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 nodes/utils/civit_metadata_helper.py diff --git a/__init__.py b/__init__.py index 51a4363..ee5e726 100644 --- a/__init__.py +++ b/__init__.py @@ -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 } diff --git a/nodes/utils/civit_metadata_helper.py b/nodes/utils/civit_metadata_helper.py new file mode 100644 index 0000000..3124085 --- /dev/null +++ b/nodes/utils/civit_metadata_helper.py @@ -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" +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4cbad90..29132e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"} diff --git a/web/js/swiss-army-knife.js b/web/js/swiss-army-knife.js index d23254e..825e122 100644 --- a/web/js/swiss-army-knife.js +++ b/web/js/swiss-army-knife.js @@ -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