This commit is contained in:
justumen
2025-01-11 12:11:11 +01:00
parent 38b83ed8f5
commit efd4105287
20 changed files with 366 additions and 19 deletions

View File

@@ -1,6 +1,6 @@
# 🔗 Comfyui : Bjornulf_custom_nodes v0.64 🔗
# 🔗 Comfyui : Bjornulf_custom_nodes v0.65 🔗
A list of 110 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters and more.
A list of 116 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters and more.
You can manage looping operations, generate randomized content, trigger logical conditions, pause and manually control your workflows and even work with external AI tools, like Ollama or Text To Speech.
# Coffee : ☕☕☕☕☕ 5/5
@@ -36,6 +36,12 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
`67.` [📝➜✨ Text to Anything](#67----text-to-anything)
`68.` [✨➜📝 Anything to Text](#68----anything-to-text)
`75.` [📝➜📝 Replace text](#75----replace-text)
`15.` [💾 Save Text](#15----save-text)
`111.` [✨➜🔢 Anything to Int](#)
`112.` [✨➜🔢 Anything to Float](#)
`113.` [📝🔪 Text split in 5](#)
`115.` [📥 Load Text From Bjornulf Folder](#)
`116.` [📥 Load Text From Path](#)
## 🔥 Text Generator 🔥
`81.` [🔥📝 Text Generator 📝🔥](#81----text-generator-)
@@ -131,6 +137,7 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
## 🚀 Load loras 🚀
`54.` [♻ Loop Lora Selector](#54----loop-lora-selector)
`55.` [🎲 Random Lora@ Selector](#55----random-lora-selector)
`114.` [📥👑 Load Lora with Path]()
## ☁ Image Creation : API / cloud / remote ☁
`106.` [☁🎨 API Image Generator (FalAI) ☁](#10)
@@ -345,6 +352,7 @@ cd /where/you/installed/ComfyUI && python main.py
- **0.62**: MASSIVE update, Text Generator nodes. (15 nodes), API nodes generate (civitai / black forest labs / fal.ai), API civit ai download models nodes, lora
- **0.63**: delete long file, useless
- **0.64**: remove "import wget", added some keywords to text generators.
- **0.65**: ❗Breaking changes : Combine Text inputs are now all optional (PLease remake your nodes, sorry.) Add 6 new nodes : any2int, any2float, load text from folder, load text from path, load lora from path. Also upgraded the Save text node.
# 📝 Nodes descriptions
@@ -491,6 +499,7 @@ Resize an image to exact dimensions. The other node will save the image to the e
**Description:**
Save the given text input to a file. Useful for logging and storing text data.
If the file already exist, it will add the text at the end of the file.
I recommend you to keep saving them in "Bjornulf/Text" (Which is in the Comfyui folder, next to output), this is where the node 116 `Load text from folder` is looking for text files.
![Save Text](screenshots/save_text.png)
@@ -1574,4 +1583,61 @@ Generate an image with the Black Forest Labs API. (flux)
**Description:**
Generate an image with the Stability API. (sd3)
![api stability](screenshots/api_stability.png)
![api stability](screenshots/api_stability.png)
#### 111 - ✨➜🔢 Anything to Int
**Description:**
Just convert anything to a valid INT. (integer)
![Anything to Int](screenshots/anything_to_int.png)
#### 112 - ✨➜🔢 Anything to Float
**Description:**
Just convert anything to a valid FLOAT. (floating number)
![Anything to Float](screenshots/anything_to_float.png)
#### 113 - 📝🔪 Text split in 5
**Description:**
Take a single input and split it in 5 with a delimiter (newline by default).
It can also ignore everything on the left side of a `=` symbol if you want to use a "variable type format".
![Text split in 5](screenshots/split_in_5.png)
#### 114 - 📥👑 Load Lora with Path
**Description:**
Load a lora by using it's path.
![load lora with path](screenshots/load_lora_with_path.png)
Here is a complex practical example using node 113, 114, 112 :
![load lora with path](screenshots/load_lora_with_path_COMPLEX.png)
#### 115 - 📥 Load Text From Bjornulf Folder
**Description:**
Just select a file from the folder `Bjornulf/Text` folder, it will recover its content.
It is made to be used with node 15 `Save Text`.
![Load Text](screenshots/load_text_from_Bjornulf.png)
#### 116 - 📥 Load Text From Path
**Description:**
Just give the path of the file, it will recover its content.
![Load Text](screenshots/load_text_requirements.png)
If you want, with `Load Text From Path` you can also recover the elements in "Bjornulf/Text" by just adding it:
![Load Text](screenshots/load_text_PATH.png)

View File

@@ -78,6 +78,8 @@ from .ollama_system_job import OllamaSystemJobSelector
from .speech_to_text import SpeechToText
from .text_to_anything import TextToAnything
from .anything_to_text import AnythingToText
from .anything_to_int import AnythingToInt
from .anything_to_float import AnythingToFloat
from .add_line_numbers import AddLineNumbers
from .ffmpeg_convert import ConvertVideo
# from .hiresfix import HiResFix
@@ -88,9 +90,16 @@ from .API_StableDiffusion import APIGenerateStability
from .API_civitai import APIGenerateCivitAI, APIGenerateCivitAIAddLORA, CivitAIModelSelectorPony, CivitAIModelSelectorSD15, CivitAIModelSelectorSDXL, CivitAIModelSelectorFLUX_S, CivitAIModelSelectorFLUX_D, CivitAILoraSelectorSD15, CivitAILoraSelectorSDXL, CivitAILoraSelectorPONY
from .API_falAI import APIGenerateFalAI
from .latent_resolution_selector import LatentResolutionSelector
from .loader_lora_with_path import LoaderLoraWithPath
from .load_text import LoadTextFromFolder, LoadTextFromPath
from .string_splitter import TextSplitin5
NODE_CLASS_MAPPINGS = {
"Bjornulf_LatentResolutionSelector": LatentResolutionSelector,
"Bjornulf_LoaderLoraWithPath": LoaderLoraWithPath,
"Bjornulf_LoadTextFromPath": LoadTextFromPath,
"Bjornulf_LoadTextFromFolder": LoadTextFromFolder,
"Bjornulf_TextSplitin5": TextSplitin5,
"Bjornulf_APIGenerateFlux": APIGenerateFlux,
"Bjornulf_APIGenerateFalAI": APIGenerateFalAI,
"Bjornulf_APIGenerateStability": APIGenerateStability,
@@ -134,6 +143,8 @@ NODE_CLASS_MAPPINGS = {
"Bjornulf_AddLineNumbers": AddLineNumbers,
"Bjornulf_TextToAnything": TextToAnything,
"Bjornulf_AnythingToText": AnythingToText,
"Bjornulf_AnythingToInt": AnythingToInt,
"Bjornulf_AnythingToFloat": AnythingToFloat,
"Bjornulf_SpeechToText": SpeechToText,
"Bjornulf_OllamaConfig": OllamaConfig,
"Bjornulf_OllamaSystemPersonaSelector": OllamaSystemPersonaSelector,
@@ -211,6 +222,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
# "Bjornulf_ImageBlend": "🎨 Image Blend",
# "Bjornulf_APIHiResCivitAI": "🎨➜🎨 API Image hires fix (CivitAI)",
# "Bjornulf_CivitAILoraSelector": "lora Civit",
"Bjornulf_LoaderLoraWithPath": "📥👑 Load Lora with Path",
"Bjornulf_TextSplitin5": "📝🔪 Text split in 5",
"Bjornulf_LatentResolutionSelector": "🩷 Empty Latent Selector",
"Bjornulf_CivitAIModelSelectorSD15": "📥 Load checkpoint SD1.5 (+Download from CivitAi)",
"Bjornulf_CivitAIModelSelectorSDXL": "📥 Load checkpoint SDXL (+Download from CivitAi)",
@@ -255,6 +268,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Bjornulf_TextToSpeech": "📝➜🔊 TTS - Text to Speech",
"Bjornulf_TextToAnything": "📝➜✨ Text to Anything",
"Bjornulf_AnythingToText": "✨➜📝 Anything to Text",
"Bjornulf_AnythingToInt": "✨➜🔢 Anything to Int",
"Bjornulf_AnythingToFloat": "✨➜🔢 Anything to Float",
"Bjornulf_TextReplace": "📝➜📝 Replace text",
"Bjornulf_AddLineNumbers": "🔢 Add line numbers",
"Bjornulf_FFmpegConfig": "⚙📹 FFmpeg Configuration 📹⚙",
@@ -311,7 +326,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Bjornulf_SaveImageToFolder": "💾🖼📁 Save Image(s) to a folder",
"Bjornulf_SaveTmpImage": "💾🖼 Save Image (tmp_api.png) ⚠️💣",
"Bjornulf_SaveText": "💾 Save Text",
# "Bjornulf_LoadText": "📥 Load Text",
"Bjornulf_LoadTextFromPath": "📥 Load Text From Path",
"Bjornulf_LoadTextFromFolder": "📥 Load Text From Bjornulf Folder",
"Bjornulf_CombineTexts": "🔗 Combine (Texts)",
"Bjornulf_imagesToVideo": "📹 images to video (FFmpeg)",
"Bjornulf_VideoPingPong": "📹 video PingPong",

28
anything_to_float.py Normal file
View File

@@ -0,0 +1,28 @@
class Everything(str):
def __ne__(self, __value: object) -> bool:
return False
class AnythingToFloat:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"anything": (Everything("*"), {"forceInput": True}),
},
}
@classmethod
def VALIDATE_INPUTS(s, input_types):
return True
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("float",)
FUNCTION = "any_to_float"
CATEGORY = "Bjornulf"
def any_to_float(self, anything):
try:
return (float(anything),)
except (ValueError, TypeError):
# Return 0.0 if conversion fails
return (0.0,)

32
anything_to_int.py Normal file
View File

@@ -0,0 +1,32 @@
class Everything(str):
def __ne__(self, __value: object) -> bool:
return False
class AnythingToInt:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"anything": (Everything("*"), {"forceInput": True}),
},
}
@classmethod
def VALIDATE_INPUTS(s, input_types):
return True
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("integer",)
FUNCTION = "any_to_int"
CATEGORY = "Bjornulf"
def any_to_int(self, anything):
try:
# Handle string inputs that might be floats
if isinstance(anything, str) and '.' in anything:
return (int(float(anything)),)
# Handle other types
return (int(anything),)
except (ValueError, TypeError):
# Return 0 if conversion fails
return (0,)

View File

@@ -1,3 +1,6 @@
class Everything(str):
def __ne__(self, __value: object) -> bool:
return False
class AnythingToText:
@classmethod
def INPUT_TYPES(s):
@@ -18,9 +21,4 @@ class AnythingToText:
def any_to_text(self, anything):
# Convert the input to string representation
return (str(anything),)
# Keep the Everything class definition as it's needed for type handling
class Everything(str):
def __ne__(self, __value: object) -> bool:
return False
return (str(anything),)

View File

@@ -5,11 +5,9 @@ class CombineTexts:
"required": {
"number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 50, "step": 1}),
"delimiter": (["newline", "comma", "space", "slash", "nothing"], {"default": "newline"}),
"text_1": ("STRING", {"forceInput": True}),
"text_2": ("STRING", {"forceInput": True}),
},
"hidden": {
**{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(3, 51)}
**{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(1, 51)}
}
}

92
load_text.py Normal file
View File

@@ -0,0 +1,92 @@
import os
class LoadTextFromFolder:
@classmethod
def INPUT_TYPES(cls):
"""Define input parameters for the node"""
default_dir = "Bjornulf/Text"
available_files = []
if os.path.exists(default_dir):
available_files = [f for f in os.listdir(default_dir)
if f.lower().endswith('.txt')]
if not available_files:
available_files = ["no_files_found"]
return {
"required": {
"text_file": (available_files, {"default": available_files[0]}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("text", "filename", "full_path")
FUNCTION = "load_text"
CATEGORY = "Bjornulf"
def load_text(self, text_file):
try:
if text_file == "no_files_found":
raise ValueError("No text files found in Bjornulf/Text folder")
filepath = os.path.join("Bjornulf/Text", text_file)
# Check if file exists
if not os.path.exists(filepath):
raise ValueError(f"File not found: {filepath}")
# Get absolute path
full_path = os.path.abspath(filepath)
# Get just the filename
filename = os.path.basename(filepath)
# Read text from file
with open(filepath, 'r', encoding='utf-8') as file:
text = file.read()
return (text, filename, full_path)
except (OSError, IOError) as e:
raise ValueError(f"Error loading file: {str(e)}")
class LoadTextFromPath:
@classmethod
def INPUT_TYPES(cls):
"""Define input parameters for the node"""
return {
"required": {
"file_path": ("STRING", {"default": "Bjornulf/Text/example.txt"}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("text", "filename", "full_path")
FUNCTION = "load_text"
CATEGORY = "Bjornulf"
def load_text(self, file_path):
try:
# Validate file extension
if not file_path.lower().endswith('.txt'):
raise ValueError("File must be a .txt file")
# Check if file exists
if not os.path.exists(file_path):
raise ValueError(f"File not found: {file_path}")
# Get absolute path
full_path = os.path.abspath(file_path)
# Get just the filename
filename = os.path.basename(file_path)
# Read text from file
with open(file_path, 'r', encoding='utf-8') as file:
text = file.read()
return (text, filename, full_path)
except (OSError, IOError) as e:
raise ValueError(f"Error loading file: {str(e)}")

49
loader_lora_with_path.py Normal file
View File

@@ -0,0 +1,49 @@
import os
import comfy.sd
import comfy.utils
class LoaderLoraWithPath:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
"lora_path": ("STRING", {"default": ""}),
"strength_model": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}),
"strength_clip": ("FLOAT", {"default": 1.0, "min": -100.0, "max": 100.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MODEL", "CLIP")
FUNCTION = "load_lora" # Added this line
CATEGORY = "Bjornulf"
def load_lora(self, model, clip, lora_path, strength_model, strength_clip):
try:
# Check if path exists
if not os.path.isfile(lora_path):
print(f"Error: Lora file not found at path: {lora_path}")
return (model, clip)
# Load the Lora file
try:
lora = comfy.utils.load_torch_file(lora_path)
except Exception as e:
print(f"Error loading Lora file: {str(e)}")
return (model, clip)
# Apply the Lora
model_lora, clip_lora = comfy.sd.load_lora_for_models(
model,
clip,
lora,
strength_model,
strength_clip
)
return (model_lora, clip_lora)
except Exception as e:
print(f"Error in load_lora: {str(e)}")
return (model, clip)

View File

@@ -1,7 +1,7 @@
[project]
name = "bjornulf_custom_nodes"
description = "110 ComfyUI nodes : Display, manipulate, and edit text, images, videos, loras, generate characters and more. Manage looping operations, generate randomized content, use logical conditions and work with external AI tools, like Ollama or Text To Speech."
version = "0.64"
description = "116 ComfyUI nodes : Display, manipulate, and edit text, images, videos, loras, generate characters and more. Manage looping operations, generate randomized content, use logical conditions and work with external AI tools, like Ollama or Text To Speech."
version = "0.65"
license = {file = "LICENSE"}
[project.urls]

View File

@@ -6,12 +6,12 @@ class SaveText:
return {
"required": {
"text": ("STRING", {"multiline": True, "forceInput": True}),
"filepath": ("STRING", {"default": "output/this_test.txt"}),
"filepath": ("STRING", {"default": "Bjornulf/Text/example.txt"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("added_text", "complete_text", "filename", "full_path")
FUNCTION = "save_text"
OUTPUT_NODE = True
CATEGORY = "Bjornulf"
@@ -27,11 +27,30 @@ class SaveText:
if directory and not os.path.exists(directory):
os.makedirs(directory)
# Get absolute path
full_path = os.path.abspath(filepath)
# Append text to file with a newline
with open(filepath, 'a', encoding='utf-8') as file:
file.write(text + '\n')
return {"ui": {"text": text}, "result": (text,)}
# Read complete file content
with open(filepath, 'r', encoding='utf-8') as file:
complete_text = file.read()
# Get just the filename
filename = os.path.basename(filepath)
# Return all requested information
return {
"ui": {"text": text},
"result": (
text, # added_text
complete_text, # complete_text
filename, # filename
full_path # full_path
)
}
except (OSError, IOError) as e:
raise ValueError(f"Error saving file: {str(e)}")

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 117 KiB

BIN
screenshots/split_in_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

49
string_splitter.py Normal file
View File

@@ -0,0 +1,49 @@
class TextSplitin5:
DELIMITER_NEWLINE = "\\n" # Literal string "\n" for display
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_string": ("STRING", {
"multiline": True,
"forceInput": True
}),
"delimiter": ("STRING", {
"default": s.DELIMITER_NEWLINE, # Show "\n" in widget
"multiline": False
}),
"ignore_before_equals": ("BOOLEAN", {
"default": False
}),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = ("part1", "part2", "part3", "part4", "part5")
FUNCTION = "split_string"
CATEGORY = "Bjornulf"
def split_string(self, input_string, delimiter, ignore_before_equals):
# Handle the special case for newline delimiter
actual_delimiter = "\n" if delimiter == self.DELIMITER_NEWLINE else delimiter
# Split the string using the delimiter
parts = input_string.split(actual_delimiter)
# Ensure we always return exactly 5 parts
result = []
for i in range(5):
if i < len(parts):
part = parts[i].strip()
# If ignore_before_equals is True and there's an equals sign
if ignore_before_equals and '=' in part:
# Take only what's after the equals sign and strip whitespace
part = part.split('=', 1)[1].strip()
result.append(part)
else:
# If no more parts, append empty string
result.append("")
# Convert to tuple and return all 5 parts
return tuple(result)