mirror of
https://github.com/justUmen/Bjornulf_custom_nodes.git
synced 2026-03-21 12:42:11 -03:00
0.51
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,5 +3,5 @@ SaveText/
|
|||||||
API_example/
|
API_example/
|
||||||
clear_vram.py
|
clear_vram.py
|
||||||
web/js/clear_vram.js
|
web/js/clear_vram.js
|
||||||
*.txt
|
speakers
|
||||||
speakers
|
*.text
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
class CustomStringType:
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(s):
|
|
||||||
return {"required": {"value": ("STRING", {"multiline": True})}}
|
|
||||||
|
|
||||||
RETURN_TYPES = ("CUSTOM_STRING",)
|
|
||||||
FUNCTION = "passthrough"
|
|
||||||
CATEGORY = "Bjornulf"
|
|
||||||
|
|
||||||
def passthrough(self, value):
|
|
||||||
return (value,)
|
|
||||||
28
README.md
28
README.md
@@ -1,6 +1,6 @@
|
|||||||
# 🔗 Comfyui : Bjornulf_custom_nodes v0.50 🔗
|
# 🔗 Comfyui : Bjornulf_custom_nodes v0.51 🔗
|
||||||
|
|
||||||
A list of 59 custom nodes for Comfyui : Display, manipulate, and edit text, images, videos, loras and more.
|
A list of 61 custom nodes for Comfyui : Display, manipulate, and edit text, images, videos, loras 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.
|
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
|
# Coffee : ☕☕☕☕☕ 5/5
|
||||||
@@ -82,6 +82,8 @@ You can manage looping operations, generate randomized content, trigger logical
|
|||||||
`44.` [🖼👈 Select an Image, Pick](#44----select-an-image-pick)
|
`44.` [🖼👈 Select an Image, Pick](#44----select-an-image-pick)
|
||||||
`46.` [🖼🔍 Image Details](#46----image-details)
|
`46.` [🖼🔍 Image Details](#46----image-details)
|
||||||
`47.` [🖼 Combine Images](#47----combine-images)
|
`47.` [🖼 Combine Images](#47----combine-images)
|
||||||
|
`60.` [🖼🖼 Merge Images/Videos 📹📹 (Horizontally)](#60)
|
||||||
|
`61.` [🖼🖼 Merge Images/Videos 📹📹 (Vertically)](#61)
|
||||||
|
|
||||||
## 🚀 Load checkpoints 🚀
|
## 🚀 Load checkpoints 🚀
|
||||||
`40.` [🎲 Random (Model+Clip+Vae) - aka Checkpoint / Model](#40----random-modelclipvae---aka-checkpoint--model)
|
`40.` [🎲 Random (Model+Clip+Vae) - aka Checkpoint / Model](#40----random-modelclipvae---aka-checkpoint--model)
|
||||||
@@ -102,6 +104,8 @@ You can manage looping operations, generate randomized content, trigger logical
|
|||||||
`52.` [🔊📹 Audio Video Sync](#52----audio-video-sync)
|
`52.` [🔊📹 Audio Video Sync](#52----audio-video-sync)
|
||||||
`58.` [📹🔗 Concat Videos](#58----concat-videos)
|
`58.` [📹🔗 Concat Videos](#58----concat-videos)
|
||||||
`59.` [📹🔊 Combine Video + Audio](#59----combine-video--audio)
|
`59.` [📹🔊 Combine Video + Audio](#59----combine-video--audio)
|
||||||
|
`60.` [🖼🖼 Merge Images/Videos 📹📹 (Horizontally)](#60)
|
||||||
|
`61.` [🖼🖼 Merge Images/Videos 📹📹 (Vertically)](#61)
|
||||||
|
|
||||||
## 🤖 AI 🤖
|
## 🤖 AI 🤖
|
||||||
`19.` [🦙 Ollama](#19----ollama)
|
`19.` [🦙 Ollama](#19----ollama)
|
||||||
@@ -256,6 +260,7 @@ cd /where/you/installed/ComfyUI && python main.py
|
|||||||
- **v0.48**: Two new nodes for loras : Random Lora Selector and Loop Lora Selector.
|
- **v0.48**: Two new nodes for loras : Random Lora Selector and Loop Lora Selector.
|
||||||
- **v0.49**: New node : Loop Sequential (Integer) - Loop through a range of integer values. (But once per workflow run), audio sync is smarter and adapt the video duration to the audio duration. add requirements.txt
|
- **v0.49**: New node : Loop Sequential (Integer) - Loop through a range of integer values. (But once per workflow run), audio sync is smarter and adapt the video duration to the audio duration. add requirements.txt
|
||||||
- **v0.50**: allow audio in Images to Video path (tmp video). Add three new nodes : Concat Videos, combine video/audio and Loop Sequential (input Lines). save text changes to write inside COmfyui folder. Fix random line from input outputing LIST. ❗ Breaking change to audio/video sync node, allowing different types as input.
|
- **v0.50**: allow audio in Images to Video path (tmp video). Add three new nodes : Concat Videos, combine video/audio and Loop Sequential (input Lines). save text changes to write inside COmfyui folder. Fix random line from input outputing LIST. ❗ Breaking change to audio/video sync node, allowing different types as input.
|
||||||
|
- **v0.50**: Fix some issues with audio/video sync node. Add two new nodes : merge images/videos vertical and horizontal.
|
||||||
|
|
||||||
# 📝 Nodes descriptions
|
# 📝 Nodes descriptions
|
||||||
|
|
||||||
@@ -823,6 +828,7 @@ Display the details of an image. (width, height, has_transparency, orientation,
|
|||||||
|
|
||||||
**Description:**
|
**Description:**
|
||||||
Combine multiple images (A single image or a list of images.)
|
Combine multiple images (A single image or a list of images.)
|
||||||
|
If you want to merge several images into a single image, check node 60 or 61.
|
||||||
|
|
||||||
There are two types of logic to "combine images". With "all_in_one" enabled, it will combine all the images into one tensor.
|
There are two types of logic to "combine images". With "all_in_one" enabled, it will combine all the images into one tensor.
|
||||||
Otherwise it will send the images one by one. (check examples below) :
|
Otherwise it will send the images one by one. (check examples below) :
|
||||||
@@ -978,3 +984,21 @@ Video : Use list of images or video path.
|
|||||||
Audio : Use audio path or audio type.
|
Audio : Use audio path or audio type.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### 60 - 🖼🖼 Merge Images/Videos 📹📹 (Horizontally)
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
Merge images or videos horizontally.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here is on possible example for videos with node 60 and 61 :
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 61 - 🖼🖼 Merge Images/Videos 📹📹 (Vertically)
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
Merge images or videos vertically.
|
||||||
|
|
||||||
|

|
||||||
@@ -62,9 +62,13 @@ from .loop_sequential_integer import LoopIntegerSequential
|
|||||||
from .loop_lines_sequential import LoopLinesSequential
|
from .loop_lines_sequential import LoopLinesSequential
|
||||||
from .concat_videos import ConcatVideos
|
from .concat_videos import ConcatVideos
|
||||||
from .combine_video_audio import CombineVideoAudio
|
from .combine_video_audio import CombineVideoAudio
|
||||||
|
from .images_merger_horizontal import MergeImagesHorizontally
|
||||||
|
from .images_merger_vertical import MergeImagesVertically
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"Bjornulf_ollamaLoader": ollamaLoader,
|
"Bjornulf_ollamaLoader": ollamaLoader,
|
||||||
|
"Bjornulf_MergeImagesHorizontally": MergeImagesHorizontally,
|
||||||
|
"Bjornulf_MergeImagesVertically": MergeImagesVertically,
|
||||||
"Bjornulf_CombineVideoAudio": CombineVideoAudio,
|
"Bjornulf_CombineVideoAudio": CombineVideoAudio,
|
||||||
"Bjornulf_ConcatVideos": ConcatVideos,
|
"Bjornulf_ConcatVideos": ConcatVideos,
|
||||||
"Bjornulf_LoopLinesSequential": LoopLinesSequential,
|
"Bjornulf_LoopLinesSequential": LoopLinesSequential,
|
||||||
@@ -128,6 +132,8 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"Bjornulf_WriteText": "✒ Write Text",
|
"Bjornulf_WriteText": "✒ Write Text",
|
||||||
|
"Bjornulf_MergeImagesHorizontally": "🖼🖼 Merge Images/Videos 📹📹 (Horizontally)",
|
||||||
|
"Bjornulf_MergeImagesVertically": "🖼🖼 Merge Images/Videos 📹📹 (Vertically)",
|
||||||
"Bjornulf_CombineVideoAudio": "📹🔊 Combine Video + Audio",
|
"Bjornulf_CombineVideoAudio": "📹🔊 Combine Video + Audio",
|
||||||
"Bjornulf_ConcatVideos": "📹🔗 Concat Videos",
|
"Bjornulf_ConcatVideos": "📹🔗 Concat Videos",
|
||||||
"Bjornulf_LoopLinesSequential": "♻📝 Loop Sequential (input Lines)",
|
"Bjornulf_LoopLinesSequential": "♻📝 Loop Sequential (input Lines)",
|
||||||
|
|||||||
@@ -231,12 +231,13 @@ class AudioVideoSync:
|
|||||||
def process_audio(self, audio_tensor, sample_rate, target_duration, original_duration,
|
def process_audio(self, audio_tensor, sample_rate, target_duration, original_duration,
|
||||||
max_speedup, max_slowdown):
|
max_speedup, max_slowdown):
|
||||||
"""Process audio to match video duration."""
|
"""Process audio to match video duration."""
|
||||||
if audio_tensor.dim() == 3:
|
# Ensure audio tensor has correct dimensions
|
||||||
audio_tensor = audio_tensor.squeeze(0)
|
if audio_tensor.dim() == 2:
|
||||||
elif audio_tensor.dim() == 1:
|
|
||||||
audio_tensor = audio_tensor.unsqueeze(0)
|
audio_tensor = audio_tensor.unsqueeze(0)
|
||||||
|
elif audio_tensor.dim() == 1:
|
||||||
|
audio_tensor = audio_tensor.unsqueeze(0).unsqueeze(0)
|
||||||
|
|
||||||
current_duration = audio_tensor.shape[1] / sample_rate
|
current_duration = audio_tensor.shape[-1] / sample_rate
|
||||||
|
|
||||||
# Calculate synchronized video duration
|
# Calculate synchronized video duration
|
||||||
if target_duration > original_duration:
|
if target_duration > original_duration:
|
||||||
@@ -256,17 +257,17 @@ class AudioVideoSync:
|
|||||||
# Adjust audio length
|
# Adjust audio length
|
||||||
if current_duration < sync_duration:
|
if current_duration < sync_duration:
|
||||||
silence_samples = int((sync_duration - current_duration) * sample_rate)
|
silence_samples = int((sync_duration - current_duration) * sample_rate)
|
||||||
silence = torch.zeros(audio_tensor.shape[0], silence_samples)
|
silence = torch.zeros(audio_tensor.shape[0], audio_tensor.shape[1], silence_samples)
|
||||||
processed_audio = torch.cat([audio_tensor, silence], dim=1)
|
processed_audio = torch.cat([audio_tensor, silence], dim=-1)
|
||||||
else:
|
else:
|
||||||
required_samples = int(sync_duration * sample_rate)
|
required_samples = int(sync_duration * sample_rate)
|
||||||
processed_audio = audio_tensor[:, :required_samples]
|
processed_audio = audio_tensor[..., :required_samples]
|
||||||
|
|
||||||
return processed_audio, sync_duration
|
return processed_audio, sync_duration
|
||||||
|
|
||||||
def save_audio(self, audio_tensor, sample_rate, target_duration, original_duration,
|
def save_audio(self, audio_tensor, sample_rate, target_duration, original_duration,
|
||||||
max_speedup, max_slowdown):
|
max_speedup, max_slowdown):
|
||||||
"""Save processed audio to file."""
|
"""Save processed audio to file and return consistent AUDIO format."""
|
||||||
timestamp = self.generate_timestamp()
|
timestamp = self.generate_timestamp()
|
||||||
output_path = os.path.join(self.sync_audio_dir, f"sync_audio_{timestamp}.wav")
|
output_path = os.path.join(self.sync_audio_dir, f"sync_audio_{timestamp}.wav")
|
||||||
|
|
||||||
@@ -275,12 +276,29 @@ class AudioVideoSync:
|
|||||||
max_speedup, max_slowdown
|
max_speedup, max_slowdown
|
||||||
)
|
)
|
||||||
|
|
||||||
torchaudio.save(output_path, processed_audio, sample_rate)
|
# Save with proper format
|
||||||
return os.path.abspath(output_path)
|
torchaudio.save(output_path, processed_audio.squeeze(0), sample_rate)
|
||||||
|
|
||||||
|
# Return consistent AUDIO format
|
||||||
|
return {
|
||||||
|
'waveform': processed_audio,
|
||||||
|
'sample_rate': sample_rate
|
||||||
|
}
|
||||||
|
|
||||||
def load_audio_from_path(self, audio_path):
|
def load_audio_from_path(self, audio_path):
|
||||||
"""Load audio from file path."""
|
"""Load audio from file path and format it consistently with AUDIO input."""
|
||||||
waveform, sample_rate = torchaudio.load(audio_path)
|
waveform, sample_rate = torchaudio.load(audio_path)
|
||||||
|
|
||||||
|
# Ensure waveform has 3 dimensions (batch, channels, samples) like AUDIO input
|
||||||
|
if waveform.dim() == 2:
|
||||||
|
waveform = waveform.unsqueeze(0) # Add batch dimension
|
||||||
|
|
||||||
|
# Convert to float32 and normalize to range [0, 1] if needed
|
||||||
|
if waveform.dtype != torch.float32:
|
||||||
|
waveform = waveform.float()
|
||||||
|
if waveform.max() > 1.0:
|
||||||
|
waveform = waveform / 32768.0 # Normalize 16-bit audio
|
||||||
|
|
||||||
return {'waveform': waveform, 'sample_rate': sample_rate}
|
return {'waveform': waveform, 'sample_rate': sample_rate}
|
||||||
|
|
||||||
def extract_frames(self, video_path):
|
def extract_frames(self, video_path):
|
||||||
@@ -297,7 +315,10 @@ class AudioVideoSync:
|
|||||||
# Load frames and convert to tensor
|
# Load frames and convert to tensor
|
||||||
frames = []
|
frames = []
|
||||||
frame_files = sorted(os.listdir(temp_dir))
|
frame_files = sorted(os.listdir(temp_dir))
|
||||||
transform = transforms.Compose([transforms.ToTensor()])
|
transform = transforms.Compose([
|
||||||
|
transforms.ToTensor(),
|
||||||
|
transforms.Lambda(lambda x: x * 255) # Scale to 0-255 range
|
||||||
|
])
|
||||||
|
|
||||||
for frame_file in frame_files:
|
for frame_file in frame_files:
|
||||||
image = Image.open(os.path.join(temp_dir, frame_file))
|
image = Image.open(os.path.join(temp_dir, frame_file))
|
||||||
@@ -307,6 +328,13 @@ class AudioVideoSync:
|
|||||||
# Stack frames into a single tensor
|
# Stack frames into a single tensor
|
||||||
frames_tensor = torch.stack(frames)
|
frames_tensor = torch.stack(frames)
|
||||||
|
|
||||||
|
# Ensure the tensor is in the correct format (B, C, H, W)
|
||||||
|
if frames_tensor.dim() == 3:
|
||||||
|
frames_tensor = frames_tensor.unsqueeze(0)
|
||||||
|
|
||||||
|
# Convert to uint8
|
||||||
|
frames_tensor = frames_tensor.byte()
|
||||||
|
|
||||||
# Clean up temporary directory
|
# Clean up temporary directory
|
||||||
for frame_file in frame_files:
|
for frame_file in frame_files:
|
||||||
os.remove(os.path.join(temp_dir, frame_file))
|
os.remove(os.path.join(temp_dir, frame_file))
|
||||||
@@ -350,25 +378,35 @@ class AudioVideoSync:
|
|||||||
sync_video_path = self.create_sync_video(
|
sync_video_path = self.create_sync_video(
|
||||||
video_path, original_duration, audio_duration, max_speedup, max_slowdown
|
video_path, original_duration, audio_duration, max_speedup, max_slowdown
|
||||||
)
|
)
|
||||||
sync_audio_path = self.save_audio(
|
|
||||||
|
# Process and save audio, getting consistent AUDIO format back
|
||||||
|
sync_audio = self.save_audio(
|
||||||
AUDIO['waveform'], AUDIO['sample_rate'], audio_duration,
|
AUDIO['waveform'], AUDIO['sample_rate'], audio_duration,
|
||||||
original_duration, max_speedup, max_slowdown
|
original_duration, max_speedup, max_slowdown
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get sync_audio_path separately
|
||||||
|
sync_audio_path = os.path.join(self.sync_audio_dir, f"sync_audio_{self.generate_timestamp()}.wav")
|
||||||
|
torchaudio.save(sync_audio_path, sync_audio['waveform'].squeeze(0), sync_audio['sample_rate'])
|
||||||
|
|
||||||
# Get final properties
|
# Get final properties
|
||||||
sync_video_duration, _, sync_frame_count = self.get_video_info(sync_video_path)
|
sync_video_duration, _, sync_frame_count = self.get_video_info(sync_video_path)
|
||||||
sync_audio_duration = torchaudio.info(sync_audio_path).num_frames / AUDIO['sample_rate']
|
sync_audio_duration = sync_audio['waveform'].shape[-1] / sync_audio['sample_rate']
|
||||||
|
|
||||||
video_frames = self.extract_frames(sync_video_path)
|
video_frames = self.extract_frames(sync_video_path)
|
||||||
|
|
||||||
|
# Convert video_frames to the format expected by ComfyUI
|
||||||
|
video_frames = video_frames.float() / 255.0
|
||||||
|
video_frames = video_frames.permute(0, 2, 3, 1)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
video_frames,
|
video_frames,
|
||||||
AUDIO,
|
sync_audio, # Now returns consistent AUDIO format
|
||||||
sync_audio_path,
|
sync_audio_path,
|
||||||
sync_video_path,
|
sync_video_path,
|
||||||
original_duration, # input_video_duration
|
original_duration,
|
||||||
sync_video_duration,
|
sync_video_duration,
|
||||||
audio_duration, # input_audio_duration
|
audio_duration,
|
||||||
sync_audio_duration,
|
sync_audio_duration,
|
||||||
sync_frame_count
|
sync_frame_count
|
||||||
)
|
)
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import random
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
class TextToStringAndSeed:
|
|
||||||
@classmethod
|
|
||||||
def INPUT_TYPES(s):
|
|
||||||
return {
|
|
||||||
"required": {
|
|
||||||
"text": ("STRING", {"forceInput": True}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
RETURN_NAMES = ("text","random_seed")
|
|
||||||
RETURN_TYPES = ("STRING", "INT")
|
|
||||||
FUNCTION = "process"
|
|
||||||
CATEGORY = "utils"
|
|
||||||
|
|
||||||
def process(self, text):
|
|
||||||
# Generate a hash from the input text
|
|
||||||
text_hash = hashlib.md5(text.encode()).hexdigest()
|
|
||||||
|
|
||||||
# Use the hash to seed the random number generator
|
|
||||||
random.seed(text_hash)
|
|
||||||
|
|
||||||
# Generate a random seed (integer)
|
|
||||||
random_seed = random.randint(0, 2**32 - 1)
|
|
||||||
|
|
||||||
# Reset the random seed to ensure randomness in subsequent calls
|
|
||||||
random.seed()
|
|
||||||
|
|
||||||
return (text, random_seed)
|
|
||||||
56
images_merger_horizontal.py
Normal file
56
images_merger_horizontal.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
class MergeImagesHorizontally:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image1": ("IMAGE",),
|
||||||
|
"image2": ("IMAGE",),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"image3": ("IMAGE",),
|
||||||
|
"image4": ("IMAGE",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "combine_images"
|
||||||
|
|
||||||
|
CATEGORY = "Bjornulf"
|
||||||
|
|
||||||
|
def combine_images(self, image1, image2, image3=None, image4=None):
|
||||||
|
# Collect all provided images
|
||||||
|
images = [image1, image2]
|
||||||
|
if image3 is not None:
|
||||||
|
images.append(image3)
|
||||||
|
if image4 is not None:
|
||||||
|
images.append(image4)
|
||||||
|
|
||||||
|
# Calculate the total width and maximum height
|
||||||
|
total_width = sum(img.shape[2] for img in images) # Sum of widths
|
||||||
|
max_height = max(img.shape[1] for img in images) # Maximum height
|
||||||
|
|
||||||
|
# Create a new tensor for the combined image
|
||||||
|
combined_image = torch.zeros((images[0].shape[0], max_height, total_width, 3), dtype=images[0].dtype, device=images[0].device)
|
||||||
|
|
||||||
|
# Paste images side by side
|
||||||
|
current_x = 0
|
||||||
|
for img in images:
|
||||||
|
b, h, w, c = img.shape
|
||||||
|
combined_image[:, :h, current_x:current_x+w, :] = img
|
||||||
|
|
||||||
|
# Blend the edge pixels if it's not the last image
|
||||||
|
# if current_x + w < total_width:
|
||||||
|
# combined_image[:, :h, current_x+w-1:current_x+w+1, :] = torch.mean(
|
||||||
|
# torch.stack([
|
||||||
|
# combined_image[:, :h, current_x+w-1:current_x+w, :],
|
||||||
|
# combined_image[:, :h, current_x+w:current_x+w+1, :]
|
||||||
|
# ]), dim=0
|
||||||
|
# )
|
||||||
|
|
||||||
|
current_x += w
|
||||||
|
|
||||||
|
return (combined_image,)
|
||||||
46
images_merger_vertical.py
Normal file
46
images_merger_vertical.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import torch
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
class MergeImagesVertically:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(s):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"image1": ("IMAGE",),
|
||||||
|
"image2": ("IMAGE",),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"image3": ("IMAGE",),
|
||||||
|
"image4": ("IMAGE",),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("IMAGE",)
|
||||||
|
FUNCTION = "combine_images"
|
||||||
|
|
||||||
|
CATEGORY = "Bjornulf"
|
||||||
|
|
||||||
|
def combine_images(self, image1, image2, image3=None, image4=None):
|
||||||
|
# Collect all provided images
|
||||||
|
images = [image1, image2]
|
||||||
|
if image3 is not None:
|
||||||
|
images.append(image3)
|
||||||
|
if image4 is not None:
|
||||||
|
images.append(image4)
|
||||||
|
|
||||||
|
# Calculate the total width and maximum height
|
||||||
|
total_width = sum(img.shape[1] for img in images)
|
||||||
|
max_height = max(img.shape[2] for img in images)
|
||||||
|
|
||||||
|
# Create a new tensor for the combined image
|
||||||
|
combined_image = torch.zeros((images[0].shape[0], total_width, max_height, 3), dtype=images[0].dtype, device=images[0].device)
|
||||||
|
|
||||||
|
# Paste images side by side
|
||||||
|
current_x = 0
|
||||||
|
for img in images:
|
||||||
|
w, h = img.shape[1:3]
|
||||||
|
combined_image[:, current_x:current_x+w, :h, :] = img[:, :, :, :]
|
||||||
|
current_x += w
|
||||||
|
|
||||||
|
return (combined_image,)
|
||||||
1
ollama_ip.txt
Normal file
1
ollama_ip.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
http://0.0.0.0:11434
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "bjornulf_custom_nodes"
|
name = "bjornulf_custom_nodes"
|
||||||
description = "Nodes: Ollama, Text to Speech, Combine Texts, Random Texts, Save image for Bjornulf LobeChat, Text with random Seed, Random line from input, Combine images, Image to grayscale (black & white), Remove image Transparency (alpha), Resize Image, ..."
|
description = "Nodes: Ollama, Text to Speech, Combine Texts, Random Texts, Save image for Bjornulf LobeChat, Text with random Seed, Random line from input, Combine images, Image to grayscale (black & white), Remove image Transparency (alpha), Resize Image, ..."
|
||||||
version = "0.50"
|
version = "0.51"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
|||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ollama
|
||||||
|
pydub
|
||||||
BIN
screenshots/merge_images_h.png
Normal file
BIN
screenshots/merge_images_h.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
BIN
screenshots/merge_images_v.png
Normal file
BIN
screenshots/merge_images_v.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
screenshots/merge_videos.png
Normal file
BIN
screenshots/merge_videos.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 425 KiB |
@@ -1,29 +0,0 @@
|
|||||||
import { app } from "../../../scripts/app.js";
|
|
||||||
|
|
||||||
app.registerExtension({
|
|
||||||
name: "Bjornulf.CustomBjornulfType",
|
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
||||||
if (nodeData.name === "Bjornulf_WriteImageCharacters") {
|
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
|
||||||
onNodeCreated?.apply(this, arguments);
|
|
||||||
const myInput = this.inputs.find(input => input.name === "BJORNULF_CHARACTER");
|
|
||||||
if (myInput) {
|
|
||||||
myInput.type = "BJORNULF_CHARACTER";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (nodeData.name === "Bjornulf_WriteImageCharacter") {
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async setup(app) {
|
|
||||||
app.registerCustomNodeType("BJORNULF_CHARACTER", (value) => {
|
|
||||||
return {
|
|
||||||
type: "BJORNULF_CHARACTER",
|
|
||||||
data: { value: value || "" },
|
|
||||||
name: "BJORNULF_CHARACTER"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { app } from "../../../scripts/app.js";
|
|
||||||
|
|
||||||
app.registerExtension({
|
|
||||||
name: "Bjornulf.CustomStringType",
|
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
||||||
if (nodeData.name === "Bjornulf_WriteImageAllInOne") {
|
|
||||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
||||||
nodeType.prototype.onNodeCreated = function () {
|
|
||||||
onNodeCreated?.apply(this, arguments);
|
|
||||||
const locationInput = this.inputs.find(input => input.name === "location");
|
|
||||||
if (locationInput) {
|
|
||||||
locationInput.type = "CUSTOM_STRING";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async setup(app) {
|
|
||||||
app.registerCustomNodeType("CUSTOM_STRING", (value) => {
|
|
||||||
return {
|
|
||||||
type: "CustomStringType",
|
|
||||||
data: { value: value || "" },
|
|
||||||
name: "CustomStringType"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Override the default onConnectionCreated method
|
|
||||||
const originalOnConnectionCreated = LGraphCanvas.prototype.onConnectionCreated;
|
|
||||||
LGraphCanvas.prototype.onConnectionCreated = function(connection, e, node_for_click) {
|
|
||||||
if (node_for_click && node_for_click.type === "WriteImageAllInOne" && connection.targetInput.name === "location") {
|
|
||||||
// Check if the connected node is not already a CustomString
|
|
||||||
if (connection.origin_node.type !== "CustomString") {
|
|
||||||
// Create a new CustomString node
|
|
||||||
const customStringNode = LiteGraph.createNode("CustomString");
|
|
||||||
// Position the new node
|
|
||||||
customStringNode.pos = [connection.origin_node.pos[0] + 200, connection.origin_node.pos[1]];
|
|
||||||
this.graph.add(customStringNode);
|
|
||||||
|
|
||||||
// Connect the new CustomString node
|
|
||||||
connection.origin_node.connect(connection.origin_slot, customStringNode, 0);
|
|
||||||
customStringNode.connect(0, node_for_click, connection.target_slot);
|
|
||||||
|
|
||||||
// Remove the original connection
|
|
||||||
connection.origin_node.disconnectOutput(connection.origin_slot, node_for_click);
|
|
||||||
|
|
||||||
return true; // Prevent the original connection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return originalOnConnectionCreated.apply(this, arguments);
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user