241 lines
11 KiB
Python
241 lines
11 KiB
Python
from .utils.colorama import ensure_colorama_initialized
|
|
ensure_colorama_initialized()
|
|
|
|
from comfy_api.latest import ComfyAPI, ComfyExtension, io
|
|
|
|
from .nodes.download_image_from_url import DownloadImageFromURL
|
|
from .nodes.save_text_file import SaveTextFile
|
|
from .nodes.get_file_path import GetFilePath
|
|
from .nodes.groq_api_llm import GroqAPILLM
|
|
from .nodes.groq_api_vlm import GroqAPIVLM
|
|
from .nodes.groq_api_alm_transcribe import GroqAPIALMTranscribe
|
|
#from .nodes.groq_api_alm_translate import GroqAPIALMTranslate
|
|
from .nodes.llm_request import LLMAPI
|
|
from .nodes.tiktoken_tokenizer import TiktokenTokenizer
|
|
from .nodes.string_cleaning import StringCleaning
|
|
from .nodes.find_replace_text import FindReplaceText
|
|
from .nodes.generate_negative_prompt import GenerateNegativePrompt
|
|
from .nodes.lora_tag_loader import LoraTagLoader
|
|
from .nodes.resolution_selector import ResolutionSelector
|
|
from .nodes.wildcard_processor import WildcardProcessor
|
|
from .nodes.wildcard_processor_advanced import WildcardProcessor as WildcardProcessorAdvanced
|
|
from .nodes.batch_wildcard_sampler import BatchWildcardSampler
|
|
from .nodes.string_text_splitter import StringTextSplitter
|
|
from .nodes.string_text_extractor import StringTextExtractor
|
|
from .nodes.format_date_time import FormatDateTime
|
|
from .nodes.load_text_image_pair_single import LoadTextImagePairSingle
|
|
from .nodes.load_text_image_pairs_list import LoadTextImagePairsList
|
|
from .nodes.metadata_extractor_single import MetadataExtractorSingle
|
|
from .nodes.metadata_extractor_list import MetadataExtractorList
|
|
from .nodes.audio_visualizer import AudioVisualizer
|
|
from .nodes.load_image_advanced import LoadImageAdvanced
|
|
from .nodes.prompt_property_extractor import PromptPropertyExtractor
|
|
|
|
from .nodes.colorful_starting_image import ColorfulStartingImage
|
|
from .nodes.load_random_checkpoint import LoadRandomCheckpoint
|
|
from .nodes.load_images import LoadImagesFromPath, register_load_images_routes
|
|
from .nodes.random_int_in_range import RandomIntInRange
|
|
from .nodes.random_float_in_range import RandomFloatInRange
|
|
from .nodes.random_bool import RandomBool
|
|
from .nodes.random_string import RandomString
|
|
from .nodes.random_seed import RandomSeed
|
|
from .nodes.random_color import RandomColor
|
|
from .nodes.string_concat import StringConcat
|
|
from .nodes.literal_bool import LiteralBool
|
|
from .nodes.literal_int import LiteralInt
|
|
from .nodes.literal_float import LiteralFloat
|
|
from .nodes.literal_string import LiteralString
|
|
from .nodes.load_image_temporarily import LoadImageTemporarily
|
|
from .nodes.ideogram4_prompt_builder import Ideogram4PromptBuilder
|
|
from .nodes.ideogram4_random_prompter import Ideogram4RandomPrompter
|
|
from .nodes.image_save_with_metadata import ImageSaveWithMetadata
|
|
from .utils.image_save_runtime_hook import install_runtime_hooks
|
|
from .utils.llm_routes import register_llm_routes
|
|
from .utils.env_manager import ensure_env_file
|
|
|
|
|
|
_api = ComfyAPI()
|
|
|
|
# Node ids used to be the emoji display strings. They are now ASCII
|
|
# (MNeMiC_*), and every old id is mapped to its new node here so workflows
|
|
# saved before the rename keep loading. Never remove an entry from this map.
|
|
LEGACY_NODE_IDS = {
|
|
"📁 Get File Path": "MNeMiC_GetFilePath",
|
|
"💾 Save Text File With Path": "MNeMiC_SaveTextFile",
|
|
"🖼️ Download Image from URL": "MNeMiC_DownloadImageFromURL",
|
|
"✨💬 Groq LLM API": "MNeMiC_GroqAPILLM",
|
|
"✨📷 Groq VLM API": "MNeMiC_GroqAPIVLM",
|
|
"✨📝 Groq ALM API - Transcribe": "MNeMiC_GroqAPIALMTranscribe",
|
|
"✨🌐 Groq ALM API - Translate [EN only]": "MNeMiC_GroqAPIALMTranslate",
|
|
"🔠 Tiktoken Tokenizer Info": "MNeMiC_TiktokenTokenizer",
|
|
"🧹 String Cleaning": "MNeMiC_StringCleaning",
|
|
"🏷️ LoRA Loader Prompt Tags": "MNeMiC_LoraTagLoader",
|
|
"📐 Resolution Image Size Selector": "MNeMiC_ResolutionSelector",
|
|
"📝 Wildcard Processor": "MNeMiC_WildcardProcessor",
|
|
"📝 Wildcard Processor Advanced": "MNeMiC_WildcardProcessorAdvanced",
|
|
"🔀 Batch Wildcard Upscale Sampler": "MNeMiC_BatchWildcardSampler",
|
|
"⚙️ Prompt Property Extractor": "MNeMiC_PromptPropertyExtractor",
|
|
"⛔ Generate Negative Prompt": "MNeMiC_GenerateNegativePrompt",
|
|
"✂️ String Text Splitter": "MNeMiC_StringTextSplitter",
|
|
"✂️ String Text Extractor": "MNeMiC_StringTextExtractor",
|
|
"📅 Format Date Time": "MNeMiC_FormatDateTime",
|
|
"🖼️📊 Metadata Extractor (Single)": "MNeMiC_MetadataExtractorSingle",
|
|
"🖼️📊 Metadata Extractor (List)": "MNeMiC_MetadataExtractorList",
|
|
"🖼️+📝 Load Text-Image Pair (Single)": "MNeMiC_LoadTextImagePairSingle",
|
|
"🖼️+📝 Load Text-Image Pairs (List)": "MNeMiC_LoadTextImagePairsList",
|
|
"🎵📊 Audio Visualizer": "MNeMiC_AudioVisualizer",
|
|
"🖼️ Load Image Advanced": "MNeMiC_LoadImageAdvanced",
|
|
"🎨 Colorful Starting Image": "MNeMiC_ColorfulStartingImage",
|
|
"🎲 Load Random Checkpoint": "MNeMiC_LoadRandomCheckpoint",
|
|
"📂 Load Images From Path": "MNeMiC_LoadImagesFromPath",
|
|
"🎲 Random Int in Range": "MNeMiC_RandomIntInRange",
|
|
"🎲 Random Float in Range": "MNeMiC_RandomFloatInRange",
|
|
"🎲 Random Bool": "MNeMiC_RandomBool",
|
|
"🎲 Random String": "MNeMiC_RandomString",
|
|
"🎲 Random Seed": "MNeMiC_RandomSeed",
|
|
"🎲 Random Color": "MNeMiC_RandomColor",
|
|
"🔗 String Concat / Append": "MNeMiC_StringConcat",
|
|
"✏️ Literal Bool": "MNeMiC_LiteralBool",
|
|
"✏️ Literal Int": "MNeMiC_LiteralInt",
|
|
"✏️ Literal Float": "MNeMiC_LiteralFloat",
|
|
"✏️ Literal String": "MNeMiC_LiteralString",
|
|
"🖼️ Load Image Temporarily": "MNeMiC_LoadImageTemporarily",
|
|
"🧩 Ideogram 4 Prompt Builder w. String Inputs": "MNeMiC_Ideogram4PromptBuilder",
|
|
"🎲 Ideogram 4 Random Prompter": "MNeMiC_Ideogram4RandomPrompter",
|
|
"💾 Save Image With Metadata": "MNeMiC_ImageSaveWithMetadata",
|
|
}
|
|
|
|
|
|
def _has_control_after_generate(node_input, input_id: str) -> bool:
|
|
"""Whether the frontend gives this widget a linked control-after-generate.
|
|
|
|
It does so when the input asks for one, and automatically for any INT
|
|
widget named `seed` or `noise_seed`.
|
|
"""
|
|
declared = getattr(node_input, "control_after_generate", None)
|
|
if declared is not None:
|
|
return bool(declared)
|
|
return isinstance(node_input, io.Int.Input) and input_id in ("seed", "noise_seed")
|
|
|
|
|
|
def _build_replacement(old_node_id: str, node_cls: type[io.ComfyNode]) -> io.NodeReplace:
|
|
"""Describe an id-only rename to ComfyUI's node replacement system.
|
|
|
|
Inputs and outputs are unchanged, so everything maps to itself — but both
|
|
mappings still have to be spelled out. The replacement builds a brand new
|
|
node and then moves connections across one entry at a time: an input that
|
|
is not listed loses its link and its widget value, and an output that is
|
|
not listed loses every link leaving it. A `None` mapping is not "keep as
|
|
is", it means "carry nothing".
|
|
|
|
`old_widget_ids` gives the widget order, which is how positional widget
|
|
values in a saved workflow JSON are matched back to input ids.
|
|
"""
|
|
schema = node_cls.GET_SCHEMA()
|
|
old_input_ids = {"input_path": "folder_path"} if schema.node_id in (
|
|
"MNeMiC_LoadTextImagePairSingle", "MNeMiC_LoadTextImagePairsList"
|
|
) else {}
|
|
input_ids = []
|
|
widget_ids = []
|
|
for node_input in schema.inputs:
|
|
input_id = getattr(node_input, "id", None)
|
|
if input_id is None:
|
|
continue
|
|
input_ids.append(input_id)
|
|
is_widget = isinstance(node_input, io.WidgetInput) and not getattr(node_input, "force_input", False)
|
|
if not is_widget:
|
|
continue # sockets are not in widgets_values
|
|
widget_ids.append(old_input_ids.get(input_id, input_id))
|
|
if _has_control_after_generate(node_input, input_id):
|
|
# The frontend attaches a linked "control after generate" widget to
|
|
# these, and it takes its own slot in the saved widgets_values
|
|
# array. old_widget_ids is matched against that array by position,
|
|
# so the slot has to be accounted for or every widget after the
|
|
# seed is read one place early. The placeholder is never used as a
|
|
# mapping id, so the new node's own control widget keeps its
|
|
# default.
|
|
widget_ids.append(f"{input_id}_control_after_generate")
|
|
return io.NodeReplace(
|
|
new_node_id=schema.node_id,
|
|
old_node_id=old_node_id,
|
|
old_widget_ids=widget_ids,
|
|
input_mapping=[{"new_id": i, "old_id": old_input_ids.get(i, i)} for i in input_ids],
|
|
output_mapping=[{"new_idx": i, "old_idx": i} for i in range(len(schema.outputs))],
|
|
)
|
|
|
|
|
|
class MnemicExtension(ComfyExtension):
|
|
async def on_load(self) -> None:
|
|
install_runtime_hooks("ImageSaveWithMetadata")
|
|
ensure_env_file()
|
|
register_llm_routes()
|
|
register_load_images_routes()
|
|
|
|
by_new_id = {}
|
|
for node_cls in await self.get_node_list():
|
|
by_new_id[node_cls.GET_SCHEMA().node_id] = node_cls
|
|
for old_node_id, new_node_id in LEGACY_NODE_IDS.items():
|
|
node_cls = by_new_id.get(new_node_id)
|
|
if node_cls is None:
|
|
continue # node is not currently registered; nothing to migrate to
|
|
await _api.node_replacement.register(_build_replacement(old_node_id, node_cls))
|
|
|
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
|
return [
|
|
GetFilePath,
|
|
SaveTextFile,
|
|
DownloadImageFromURL,
|
|
GroqAPILLM,
|
|
GroqAPIVLM,
|
|
GroqAPIALMTranscribe,
|
|
#GroqAPIALMTranslate,
|
|
LLMAPI,
|
|
TiktokenTokenizer,
|
|
StringCleaning,
|
|
FindReplaceText,
|
|
LoraTagLoader,
|
|
ResolutionSelector,
|
|
WildcardProcessor,
|
|
WildcardProcessorAdvanced,
|
|
BatchWildcardSampler,
|
|
PromptPropertyExtractor,
|
|
GenerateNegativePrompt,
|
|
StringTextSplitter,
|
|
StringTextExtractor,
|
|
FormatDateTime,
|
|
MetadataExtractorSingle,
|
|
MetadataExtractorList,
|
|
LoadTextImagePairSingle,
|
|
LoadTextImagePairsList,
|
|
AudioVisualizer,
|
|
LoadImageAdvanced,
|
|
ColorfulStartingImage,
|
|
LoadRandomCheckpoint,
|
|
LoadImagesFromPath,
|
|
RandomIntInRange,
|
|
RandomFloatInRange,
|
|
RandomBool,
|
|
RandomString,
|
|
RandomSeed,
|
|
RandomColor,
|
|
StringConcat,
|
|
LiteralBool,
|
|
LiteralInt,
|
|
LiteralFloat,
|
|
LiteralString,
|
|
LoadImageTemporarily,
|
|
Ideogram4PromptBuilder,
|
|
Ideogram4RandomPrompter,
|
|
ImageSaveWithMetadata,
|
|
]
|
|
|
|
|
|
async def comfy_entrypoint() -> MnemicExtension:
|
|
return MnemicExtension()
|
|
|
|
|
|
WEB_DIRECTORY = "./web"
|
|
__all__ = ["comfy_entrypoint", "WEB_DIRECTORY"]
|
|
|
|
print("\033[34m⚡ MNeMiC Nodes: \033[92mLoaded\033[0m")
|