mirror of
https://github.com/justUmen/Bjornulf_custom_nodes.git
synced 2026-03-21 12:42:11 -03:00
0.71
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -7,4 +7,7 @@ speakers
|
||||
*.text
|
||||
web/js/*.txt
|
||||
ScriptsPerso/
|
||||
civitai/NSFW_*
|
||||
civitai/NSFW_*
|
||||
pickme.py
|
||||
web/js/pickme.js
|
||||
todo.py
|
||||
88
README.md
88
README.md
@@ -1,6 +1,6 @@
|
||||
# 🔗 Comfyui : Bjornulf_custom_nodes v0.70 🔗
|
||||
# 🔗 Comfyui : Bjornulf_custom_nodes v0.71 🔗
|
||||
|
||||
A list of 128 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters and more.
|
||||
A list of 133 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.
|
||||
|
||||
# Watch Video Intro (Quick overview 28 minutes) :
|
||||
@@ -27,12 +27,12 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
|
||||
`73.` [👁 Show (String/Text)](#73----show-stringtext)
|
||||
`74.` [👁 Show (JSON)](#74----show-json)
|
||||
`126.` [📒 Note](#126----note)
|
||||
`127.` [🖼📒 Image Note](#127----image-note)
|
||||
`127.` [🖼📒 Image Note (Load image)](#127)
|
||||
`128.` [🖼👁 Preview (first) image](#128----preview-first-image)
|
||||
|
||||
## ✒ Text ✒
|
||||
`2.` [✒ Write Text](#2----write-text)
|
||||
`3.` [✒🗔 Advanced Write Text (+ 🎲 random selection and 🅰️ variables)](#3----advanced-write-text---random-selection-and-🅰%EF%B8%8F-variables)
|
||||
`3.` [✒🗔🅰️ Advanced Write Text (+ 🎲 random option)](#3)
|
||||
`4.` [🔗 Combine Texts](#4----combine-texts)
|
||||
`15.` [💾 Save Text](#15----save-text)
|
||||
`26.` [🎲 Random line from input](#26----random-line-from-input)
|
||||
@@ -46,6 +46,7 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
|
||||
`111.` [✨➜🔢 Anything to Int](#111----anything-to-int)
|
||||
`112.` [✨➜🔢 Anything to Float](#112----anything-to-float)
|
||||
`113.` [📝🔪 Text split in 5](#113----text-split-in-5)
|
||||
`.` [📝🔪 Text split in 10](#1)
|
||||
`115.` [📥 Load Text From Bjornulf Folder](115----load-text-from-bjornulf-folder)
|
||||
`116.` [📥 Load Text From Path](#116----load-text-from-path)
|
||||
`117.` [📝👈 Line selector (🎲 or ♻ or ♻📑)](#117)
|
||||
@@ -80,7 +81,7 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
|
||||
`27.` [♻ Loop (All Lines from input)](#27----loop-all-lines-from-input)
|
||||
`33.` [♻ Loop (All Lines from input 🔗 combine by lines)](#33----loop-all-lines-from-input--combine-by-lines)
|
||||
`38.` [♻🖼 Loop (Images)](#38----loop-images)
|
||||
`39.` [♻ Loop (✒🗔 Advanced Write Text + 🅰️ variables)](#39----loop--advanced-write-text)
|
||||
`39.` [♻ Loop (✒🗔🅰️ Advanced Write Text)](#39)
|
||||
`42.` [♻ Loop (Model+Clip+Vae) - aka Checkpoint / Model](#42----loop-modelclipvae---aka-checkpoint--model)
|
||||
`53.` [♻ Loop Load checkpoint (Model Selector)](#53----loop-load-checkpoint-model-selector)
|
||||
`54.` [♻👑 Loop Lora Selector](#54----loop-lora-selector)
|
||||
@@ -95,7 +96,7 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
|
||||
`96.` [♻👗 List Looper (Text Generator Outfits Female)](#8)
|
||||
|
||||
## 🎲 Randomization 🎲
|
||||
`3.` [✒🗔 Advanced Write Text (+ 🎲 random selection and 🅰️ variables)](#3----advanced-write-text---random-selection-and-🅰%EF%B8%8F-variables)
|
||||
`3.` [✒🗔🅰️ Advanced Write Text (+ 🎲 random option)](#3)
|
||||
`5.` [🎲 Random (Texts)](#5----random-texts)
|
||||
`26.` [🎲 Random line from input](#26----random-line-from-input)
|
||||
`28.` [🔢🎲 Text with random Seed](#28----text-with-random-seed)
|
||||
@@ -104,19 +105,19 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
|
||||
`41.` [🎲 Random Load checkpoint (Model Selector)](#41----random-load-checkpoint-model-selector)
|
||||
`48.` [🔀🎲 Text scrambler (🧑 Character)](#48----text-scrambler--character)
|
||||
`55.` [🎲👑 Random Lora Selector](#55----random-lora-selector)
|
||||
`117.` [📝👈 Line selector (🎲 or ♻ or ♻📑)](#117----line-selector--or--or-)
|
||||
`117.` [📝👈🅰️ Line selector (🎲 or ♻ or ♻📑)](#117)
|
||||
|
||||
## 🖼💾 Save Image / Text 💾🖼
|
||||
`16.` [💾🖼💬 Save image for Bjornulf LobeChat](#16----save-image-for-bjornulf-lobechat-for-my-custom-lobe-chat)
|
||||
`17.` [💾🖼 Save image as `tmp_api.png` Temporary API](#17----save-image-as-tmp_apipng-temporary-api-%EF%B8%8F)
|
||||
`18.` [💾🖼📁 Save image to a chosen folder name](#18----save-image-to-a-chosen-folder-name)
|
||||
`14.` [💾🖼 Save Exact name](#1314------resize-and-save-exact-name-%EF%B8%8F)
|
||||
`123.` [💾 Save Global Variables](#123----save-global-variables)
|
||||
`123.` [💾🅰️ Save Global Variables](#123)
|
||||
|
||||
## 🖼📥 Load Image / Text 📥🖼
|
||||
`29.` [📥🖼 Load Image with Transparency ▢](#29----load-image-with-transparency-)
|
||||
`43.` [📥🖼📂 Load Images from output folder](#43----load-images-from-output-folder)
|
||||
`124.` [📥 Load Global Variables](#124----load-global-variables)
|
||||
`124.` [📥🅰️ Load Global Variables](#124)
|
||||
|
||||
## 🖼 Image - others 🖼
|
||||
`13.` [📏 Resize Image](#1314------resize-and-save-exact-name-%EF%B8%8F)
|
||||
@@ -138,6 +139,11 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
|
||||
`70.` [📏 Resize Image Percentage](#70----resize-image-percentage)
|
||||
`80.` [🩷 Empty Latent Selector](#80----empty-latent-selector)
|
||||
|
||||
## 🅰️ Variables 🅰️
|
||||
`117.` [📝👈🅰️ Line selector (🎲 or ♻ or ♻📑)](#117)
|
||||
`123.` [💾🅰️ Save Global Variables](#123)
|
||||
`124.` [📥🅰️ Load Global Variables](#124)
|
||||
|
||||
## 🚀 Load checkpoints 🚀
|
||||
`40.` [🎲 Random (Model+Clip+Vae) - aka Checkpoint / Model](#40----random-modelclipvae---aka-checkpoint--model)
|
||||
`41.` [🎲 Random Load checkpoint (Model Selector)](#41----random-load-checkpoint-model-selector)
|
||||
@@ -211,7 +217,7 @@ Support me and my work : ❤️❤️❤️ <https://ko-fi.com/bjornulf> ❤️
|
||||
## 🧍 Manual user Control 🧍
|
||||
`35.` [⏸️ Paused. Resume or Stop, Pick 👇](#35---%EF%B8%8F-paused-resume-or-stop-)
|
||||
`36.` [⏸️ Paused. Select input, Pick 👇](#36---%EF%B8%8F-paused-select-input-pick-one)
|
||||
`117.` [📝👈 Line selector (🎲 or ♻ or ♻📑)](#117----line-selector--or--or-)
|
||||
`117.` [📝👈🅰️ Line selector (🎲 or ♻ or ♻📑)](#117)
|
||||
|
||||
## 🧠 Logic / Conditional Operations 🧠
|
||||
`45.` [🔀 If-Else (input / compare_with)](#45----if-else-input--compare_with)
|
||||
@@ -388,7 +394,8 @@ cd /where/you/installed/ComfyUI && python main.py
|
||||
Text replace now have multine option for regex. (https://github.com/justUmen/Bjornulf_custom_nodes/issues/17) - can remove <think> tag from ollama.
|
||||
8 new nodes : "🖼👁 Preview (first) image", "💾 Huggingface Downloader", "👑 Combine Loras, Lora stack", "📥 Load Global Variables", "💾 Save Global Variables", "📝👈 Model-Clip-Vae selector (🎲 or ♻ or ♻📑)", "📒 Note", "🖼📒 Image Note".
|
||||
Fix a lot of code everywhere, a little better logging system, etc...
|
||||
WIP : Rewrite of all my ffmpeg nodes. (Still need improvements and fixes, will do that in 0.71) Maybe don't use them yet...
|
||||
WIP : Rewrite of all my ffmpeg nodes. (Still need improvements and fixes, will do that in 0.71?) Maybe don't use them yet...
|
||||
- **0.71**: ❗Breaking changes for Global variable nodes. (add to global variable system a "filename", which is a a separate global variable file.) bug fix speech to text node, 5 new nodes 129-133. combine text limit raised to 100. improve Save image in folder node.
|
||||
|
||||
# 📝 Nodes descriptions
|
||||
|
||||
@@ -410,7 +417,7 @@ Simple node to write text.
|
||||
|
||||

|
||||
|
||||
## 3 - ✒🗔 Advanced Write Text (+ 🎲 random selection and 🅰️ variables)
|
||||
## 3 - ✒🗔🅰️ Advanced Write Text (+ 🎲 random option)
|
||||
|
||||
**Description:**
|
||||
Advanced Write Text node allows for special syntax to accept random variants, like `{hood|helmet}` will randomly choose between hood or helmet.
|
||||
@@ -852,7 +859,7 @@ Loop over a list of images.
|
||||
Usage example : You have a list of images, and you want to apply the same process to all of them.
|
||||
Above is an example of the loop images node sending them to an Ipadapter workflow. (Same seed of course.)
|
||||
|
||||
### 39 - ♻ Loop (✒🗔 Advanced Write Text)
|
||||
### 39 - ♻ Loop (✒🗔🅰️ Advanced Write Text)
|
||||
|
||||
**Description:**
|
||||
If you need a quick loop but you don't want something too complex with a loop node, you can use this combined write text + loop.
|
||||
@@ -1680,7 +1687,7 @@ If you want, with `Load Text From Path` you can also recover the elements in "Bj
|
||||
|
||||

|
||||
|
||||
#### 117 - 📝👈 Line selector (🎲 or ♻ or ♻📑)
|
||||
#### 117 - 📝👈🅰️ Line selector (🎲 or ♻ or ♻📑)
|
||||
|
||||
**Description:**
|
||||
|
||||
@@ -1734,7 +1741,7 @@ If you want to have multiple loras in a single node, well this is it.
|
||||
|
||||

|
||||
|
||||
#### 123 - 💾 Save Global Variables
|
||||
#### 123 - 💾🅰️ Save Global Variables
|
||||
|
||||
**Description:**
|
||||
So if you know how to use variables with my nodes, this node gives you the opportunity to create global variables.
|
||||
@@ -1742,7 +1749,7 @@ This node is very simple, it will just append (or overwrite) the file : `Bjornul
|
||||
|
||||

|
||||
|
||||
#### 124 - 📥 Load Global Variables
|
||||
#### 124 - 📥🅰️ Load Global Variables
|
||||
|
||||
**Description:**
|
||||
This node will load the global variables as text from the file `Bjornulf/GlobalVariables.txt`.
|
||||
@@ -1775,6 +1782,7 @@ You can use this node to have it show a previously generated image and some cust
|
||||

|
||||
|
||||
You can use the text to display the prompt used to generate the image for example.
|
||||
It's behavior is like a "Preview image" node. (See node 130 if you want a behavior similar to "Load image")
|
||||
|
||||
Sometimes I want to display an image to explain what something specific is doing visually. (For example a stack of loras will have a specific style.)
|
||||
Here is a complex example on how i use that, for a list of loras stacks. (I then "select" a style by using node `125 - Model-Clip-Vae selector`)
|
||||
@@ -1792,3 +1800,51 @@ Very useful for testing when working with videos.
|
||||
Below is a visual example of what I just said :
|
||||
|
||||

|
||||
|
||||
#### 129 - 📌🅰️ Set Variable from Text
|
||||
|
||||
**Description:**
|
||||
This node will just quickly transform a text in another text which can be quickly used for all my variables nodes.
|
||||
Here is an example below with "Advanced write text", but you can use with all of them, global variables, etc...
|
||||
|
||||

|
||||
|
||||
#### 130 - 📥🖼📒 Image Note (Load image)
|
||||
|
||||
**Description:**
|
||||
This node is quite similar to the node 127. But this one uses LoadImage instead of a preview system.
|
||||
So if you want to have a "preview" before you launch workflow one time, you can use this one.
|
||||
It's behaviour is like a "Load image" node.
|
||||
|
||||

|
||||
|
||||
#### 131 - ✒👉 Write Pick Me Chain
|
||||
|
||||
**Description:**
|
||||
So this is a new "write text" node.
|
||||
But with a twist. You can connect them to each other and when clicking on the PICK ME button, it will disable all other write text node of the chain and activate only the one you click on. (It will turn green.)
|
||||
So with this node, you can switch from one prompt to another by the click of a button !!
|
||||
Not limited to one line, you can use list, variables, etc... but below is a simple example :
|
||||
|
||||

|
||||
|
||||
#### 132 - 📝🔪 Text split in 10
|
||||
|
||||
**Description:**
|
||||
Same as node 113, but split in 10 parts.
|
||||
One day I had 6, and got stuck with the split in 5 node, so i guess it can be useful sometimes, let's make one with 10...
|
||||
|
||||

|
||||
|
||||
#### 133 - 🖼👁 Preview 1-4 images (compare)
|
||||
|
||||
**Description:**
|
||||
Cool node that you can use to compare several images.
|
||||
The middle is a cursor that you can move wherever you want by just clicking on the image.
|
||||
Below is an example, you can see that at this size/resolution, 25% is almost as good as the initial image.
|
||||
|
||||

|
||||
|
||||
Here is a zoom on the same image :
|
||||
|
||||

|
||||
|
||||
35
__init__.py
35
__init__.py
@@ -92,11 +92,11 @@ 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
|
||||
from .string_splitter import TextSplitin5, TextSplitin10
|
||||
from .line_selector import LineSelector
|
||||
from .text_to_speech_kokoro import KokoroTTS
|
||||
from .note_text import DisplayNote
|
||||
from .note_image import ImageNote
|
||||
from .note_image import ImageNote, ImageNoteLoadImage
|
||||
from .model_clip_vae_selector import ModelClipVaeSelector
|
||||
from .global_variables import LoadGlobalVariables, SaveGlobalVariables
|
||||
from .lora_stacks import AllLoraSelector
|
||||
@@ -105,7 +105,18 @@ from .preview_first_image import PreviewFirstImage
|
||||
# from .video_latent import VideoLatentResolutionSelector
|
||||
# from .empty_latent_video import EmptyVideoLatentWithSingle
|
||||
# from .text_generator_t2v import TextGeneratorText2Video
|
||||
from .images_compare import FourImageViewer
|
||||
# from .pickme import WriteTextPickMe, PickMe
|
||||
from .write_pickme_chain import WriteTextPickMeChain
|
||||
# from .todo import ToDoList
|
||||
from .text_to_variable import TextToVariable
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"Bjornulf_TextToVariable": TextToVariable,
|
||||
# "Bjornulf_ToDoList": ToDoList,
|
||||
# "Bjornulf_WriteTextPickMe": WriteTextPickMe,
|
||||
"Bjornulf_WriteTextPickMeChain": WriteTextPickMeChain,
|
||||
# "Bjornulf_PickMe": PickMe,
|
||||
"Bjornulf_FourImageViewer": FourImageViewer,
|
||||
"Bjornulf_PreviewFirstImage": PreviewFirstImage,
|
||||
"Bjornulf_HuggingFaceDownloader": HuggingFaceDownloader,
|
||||
# "Bjornulf_VideoLatentResolutionSelector": VideoLatentResolutionSelector,
|
||||
@@ -115,6 +126,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Bjornulf_ModelClipVaeSelector": ModelClipVaeSelector,
|
||||
"Bjornulf_DisplayNote": DisplayNote,
|
||||
"Bjornulf_ImageNote": ImageNote,
|
||||
"Bjornulf_ImageNoteLoadImage": ImageNoteLoadImage,
|
||||
"Bjornulf_LineSelector": LineSelector,
|
||||
# "Bjornulf_EmptyVideoLatentWithSingle": EmptyVideoLatentWithSingle,
|
||||
"Bjornulf_XTTSConfig": XTTSConfig,
|
||||
@@ -125,6 +137,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Bjornulf_LoadTextFromPath": LoadTextFromPath,
|
||||
"Bjornulf_LoadTextFromFolder": LoadTextFromFolder,
|
||||
"Bjornulf_TextSplitin5": TextSplitin5,
|
||||
"Bjornulf_TextSplitin10": TextSplitin10,
|
||||
"Bjornulf_APIGenerateFlux": APIGenerateFlux,
|
||||
"Bjornulf_APIGenerateFalAI": APIGenerateFalAI,
|
||||
"Bjornulf_APIGenerateStability": APIGenerateStability,
|
||||
@@ -244,14 +257,21 @@ NODE_CLASS_MAPPINGS = {
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Bjornulf_TextToVariable": "📌🅰️ Set Variable from Text",
|
||||
# "Bjornulf_ToDoList": "ToDoList",
|
||||
# "Bjornulf_WriteTextPickMe": "✒👉 Write Pick Me",
|
||||
"Bjornulf_WriteTextPickMeChain": "✒👉 Write Pick Me Chain",
|
||||
# "Bjornulf_PickMe": "✋ Recover Pick Me ! ✋",
|
||||
"Bjornulf_FourImageViewer": "🖼👁 Preview 1-4 images (compare)",
|
||||
"Bjornulf_PreviewFirstImage": "🖼👁 Preview (first) image",
|
||||
"Bjornulf_HuggingFaceDownloader": "💾 Huggingface Downloader",
|
||||
"Bjornulf_AllLoraSelector": "👑 Combine Loras, Lora stack",
|
||||
"Bjornulf_LoadGlobalVariables": "📥 Load Global Variables",
|
||||
"Bjornulf_SaveGlobalVariables": "💾 Save Global Variables",
|
||||
"Bjornulf_LoadGlobalVariables": "📥🅰️ Load Global Variables",
|
||||
"Bjornulf_SaveGlobalVariables": "💾🅰️ Save Global Variables",
|
||||
"Bjornulf_ModelClipVaeSelector": "📝👈 Model-Clip-Vae selector (🎲 or ♻ or ♻📑)",
|
||||
"Bjornulf_DisplayNote": "📒 Note",
|
||||
"Bjornulf_ImageNote": "🖼📒 Image Note",
|
||||
"Bjornulf_ImageNoteLoadImage": "📥🖼📒 Image Note (Load image)",
|
||||
# "Bjornulf_VideoLatentResolutionSelector": "🩷📹 Empty Video Latent Selector",
|
||||
# "Bjornulf_EmptyVideoLatentWithSingle": "Bjornulf_EmptyVideoLatentWithSingle",
|
||||
"Bjornulf_XTTSConfig": "🔊 TTS Configuration ⚙",
|
||||
@@ -261,10 +281,11 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
# "Bjornulf_APIHiResCivitAI": "🎨➜🎨 API Image hires fix (CivitAI)",
|
||||
# "Bjornulf_CivitAILoraSelector": "lora Civit",
|
||||
"Bjornulf_KokoroTTS": "📝➜🔊 Kokoro - Text to Speech",
|
||||
"Bjornulf_LineSelector": "📝👈 Line selector (🎲 or ♻ or ♻📑)",
|
||||
"Bjornulf_LineSelector": "📝👈🅰️ Line selector (🎲 or ♻ or ♻📑)",
|
||||
"Bjornulf_LoaderLoraWithPath": "📥👑 Load Lora with Path",
|
||||
# "Bjornulf_TextGeneratorText2Video": "🔥📝📹 Text Generator for text to video 📹📝🔥",
|
||||
"Bjornulf_TextSplitin5": "📝🔪 Text split in 5",
|
||||
"Bjornulf_TextSplitin10": "📝🔪 Text split in 10",
|
||||
"Bjornulf_LatentResolutionSelector": "🩷 Empty Latent Selector",
|
||||
"Bjornulf_CivitAIModelSelectorSD15": "📥 Load checkpoint SD1.5 (+Download from CivitAi)",
|
||||
"Bjornulf_CivitAIModelSelectorSDXL": "📥 Load checkpoint SDXL (+Download from CivitAi)",
|
||||
@@ -334,8 +355,8 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"Bjornulf_VideoToImagesList": "📹➜🖼 Video Path to Images (Load video)",
|
||||
"Bjornulf_AudioVideoSync": "🔊📹 Audio Video Sync",
|
||||
"Bjornulf_ScramblerCharacter": "🔀🎲 Text scrambler (🧑 Character)",
|
||||
"Bjornulf_WriteTextAdvanced": "✒🗔 Advanced Write Text",
|
||||
"Bjornulf_LoopWriteText": "♻ Loop (✒🗔 Advanced Write Text)",
|
||||
"Bjornulf_WriteTextAdvanced": "✒🗔🅰️ Advanced Write Text",
|
||||
"Bjornulf_LoopWriteText": "♻ Loop (✒🗔🅰️ Advanced Write Text)",
|
||||
"Bjornulf_LoopModelClipVae": "♻ Loop (Model+Clip+Vae)",
|
||||
"Bjornulf_LoopImages": "♻🖼 Loop (Images)",
|
||||
"Bjornulf_CombineTextsByLines": "♻ Loop (All Lines from input 🔗 combine by lines)",
|
||||
|
||||
@@ -3,11 +3,11 @@ class CombineTexts:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 50, "step": 1}),
|
||||
"delimiter": (["newline", "comma", "space", "slash", "nothing"], {"default": "newline"}),
|
||||
"number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 100, "step": 1}),
|
||||
"delimiter": (["newline", "comma", "space", "slash", "backslash", "nothing"], {"default": "newline"}),
|
||||
},
|
||||
"hidden": {
|
||||
**{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(1, 51)}
|
||||
**{f"text_{i}": ("STRING", {"forceInput": True}) for i in range(1, 101)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ class CombineTexts:
|
||||
return " "
|
||||
elif delimiter == "slash":
|
||||
return "/"
|
||||
elif delimiter == "backslash":
|
||||
return "\\"
|
||||
elif delimiter == "nothing":
|
||||
return ""
|
||||
else:
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import os
|
||||
import re
|
||||
import folder_paths
|
||||
import logging
|
||||
|
||||
class Everything(str):
|
||||
def __ne__(self, __value: object) -> bool:
|
||||
return False
|
||||
|
||||
# Define VAR_PATTERN at module level if not already defined
|
||||
VAR_PATTERN = re.compile(r'^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$')
|
||||
|
||||
class SaveGlobalVariables:
|
||||
def __init__(self):
|
||||
self.output_dir = os.path.join(folder_paths.base_path, 'Bjornulf')
|
||||
self.filename = os.path.join(self.output_dir, 'GlobalVariables.txt')
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
self.base_dir = os.path.join(folder_paths.base_path, 'Bjornulf')
|
||||
self.global_vars_dir = os.path.join(self.base_dir, 'GlobalVariables')
|
||||
os.makedirs(self.global_vars_dir, exist_ok=True)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -15,63 +23,138 @@ class SaveGlobalVariables:
|
||||
"variables": ("STRING", {"multiline": True, "default": ""}),
|
||||
"mode": (["append", "overwrite"], {"default": "append"}),
|
||||
},
|
||||
"optional": {
|
||||
"filename": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("connect_to_workflow",)
|
||||
FUNCTION = "save_variables"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "Bjornulf"
|
||||
|
||||
def save_variables(self, variables, mode):
|
||||
# Clean and validate input
|
||||
new_lines = set(line.strip() for line in variables.split('\n') if line.strip())
|
||||
def save_variables(self, variables, mode, filename=""):
|
||||
# Determine target file path
|
||||
if filename.strip():
|
||||
filename_clean = os.path.basename(filename.strip())
|
||||
if not filename_clean.endswith('.txt'):
|
||||
filename_clean += '.txt'
|
||||
file_path = os.path.join(self.global_vars_dir, filename_clean)
|
||||
else:
|
||||
file_path = os.path.join(self.base_dir, 'GlobalVariables.txt')
|
||||
|
||||
if mode == "overwrite":
|
||||
with open(self.filename, 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(new_lines) + '\n')
|
||||
else: # append mode
|
||||
if os.path.exists(self.filename):
|
||||
with open(self.filename, 'r', encoding='utf-8') as f:
|
||||
existing_lines = set(line.strip() for line in f.readlines() if line.strip())
|
||||
# Validate and parse input variables
|
||||
valid_vars = {}
|
||||
errors = []
|
||||
for line in variables.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
match = VAR_PATTERN.match(line)
|
||||
if match:
|
||||
var_name, var_value = match.groups()
|
||||
logging.info(f"VALID syntax for Variable : {line}")
|
||||
valid_vars[var_name] = line
|
||||
else:
|
||||
existing_lines = set()
|
||||
logging.info(f"Invalid syntax for Variable : {line}")
|
||||
errors.append(f"Invalid syntax: {line}")
|
||||
|
||||
# Add only new unique lines
|
||||
unique_lines = new_lines - existing_lines
|
||||
if unique_lines:
|
||||
with open(self.filename, 'a', encoding='utf-8') as f:
|
||||
f.write('\n'.join(unique_lines) + '\n')
|
||||
if errors:
|
||||
print("\n".join(errors))
|
||||
|
||||
# Merge based on mode
|
||||
if mode == "append":
|
||||
# Always read existing variables first
|
||||
existing_vars = {}
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if match := VAR_PATTERN.match(line):
|
||||
var_name = match.group(1)
|
||||
existing_vars[var_name] = line
|
||||
except Exception as e:
|
||||
logging.info(f"Error reading existing file: {e}")
|
||||
merged_vars = {**existing_vars, **valid_vars}
|
||||
else: # overwrite
|
||||
merged_vars = valid_vars
|
||||
|
||||
return ()
|
||||
# Ensure directory exists before writing
|
||||
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
||||
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
if merged_vars:
|
||||
f.write('\n'.join(merged_vars.values()) + '\n')
|
||||
else:
|
||||
f.write('') # Create empty file if no variables
|
||||
except Exception as e:
|
||||
logging.info(f"Error writing to file: {e}")
|
||||
|
||||
return ("",)
|
||||
|
||||
class LoadGlobalVariables:
|
||||
def __init__(self):
|
||||
self.output_dir = os.path.join(folder_paths.base_path, 'Bjornulf')
|
||||
self.filename = os.path.join(self.output_dir, 'GlobalVariables.txt')
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
self.base_dir = os.path.join(folder_paths.base_path, 'Bjornulf')
|
||||
self.global_vars_dir = os.path.join(self.base_dir, 'GlobalVariables')
|
||||
os.makedirs(self.global_vars_dir, exist_ok=True)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {
|
||||
"seed": ("INT", {
|
||||
"default": -1,
|
||||
"min": -1,
|
||||
"max": 0x7FFFFFFFFFFFFFFF
|
||||
}),
|
||||
}}
|
||||
var_files = []
|
||||
try:
|
||||
var_files = [f[:-4] for f in os.listdir(cls.global_vars_dir())
|
||||
if f.endswith('.txt') and os.path.isfile(os.path.join(cls.global_vars_dir(), f))]
|
||||
var_files.sort()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"seed": ("INT", {"default": -1, "min": -1, "max": 0x7FFFFFFFFFFFFFFF}),
|
||||
},
|
||||
"optional": {
|
||||
"filename": ("STRING", {"default": ""}),
|
||||
"file_list": (["default"] + var_files, {"default": "default"}),
|
||||
"connect_to_workflow": (Everything("*"), {"forceInput": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("variables",)
|
||||
FUNCTION = "load_variables"
|
||||
CATEGORY = "Bjornulf"
|
||||
|
||||
def load_variables(self, seed):
|
||||
if not os.path.exists(self.filename):
|
||||
@classmethod
|
||||
def global_vars_dir(cls):
|
||||
return os.path.join(folder_paths.base_path, 'Bjornulf', 'GlobalVariables')
|
||||
|
||||
def load_variables(self, seed, connect_to_workflow="", filename="", file_list="default"):
|
||||
# First check if filename is provided and not empty
|
||||
if filename.strip():
|
||||
target_file = filename.strip()
|
||||
else:
|
||||
# If filename is empty, use file_list
|
||||
target_file = file_list.strip()
|
||||
|
||||
if target_file and target_file != "default":
|
||||
# Ensure .txt extension
|
||||
if not target_file.endswith('.txt'):
|
||||
target_file += '.txt'
|
||||
# Load from GlobalVariables subdirectory
|
||||
file_path = os.path.join(self.global_vars_dir, target_file)
|
||||
else:
|
||||
# Load default GlobalVariables.txt from base directory
|
||||
file_path = os.path.join(self.base_dir, 'GlobalVariables.txt')
|
||||
|
||||
# Return empty string if file doesn't exist
|
||||
if not os.path.exists(file_path):
|
||||
return ("",)
|
||||
|
||||
with open(self.filename, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
# Read and return file contents
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read().strip()
|
||||
|
||||
os.sync() # Ensures that any pending file writes are flushed to disk
|
||||
return (content,)
|
||||
|
||||
return (content,)
|
||||
|
||||
33
images_compare.py
Normal file
33
images_compare.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from nodes import PreviewImage
|
||||
|
||||
class FourImageViewer(PreviewImage):
|
||||
"""A node that compares four images in the UI."""
|
||||
|
||||
NAME = 'Four Image Comparer'
|
||||
CATEGORY = "Bjornulf"
|
||||
FUNCTION = "compare_images"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"image_1": ("IMAGE",),
|
||||
"image_2": ("IMAGE",),
|
||||
"image_3": ("IMAGE",),
|
||||
"image_4": ("IMAGE",),
|
||||
}
|
||||
}
|
||||
|
||||
def compare_images(self, **kwargs):
|
||||
result = {"ui": {}}
|
||||
|
||||
for i in range(1, 5):
|
||||
image_key = f"image_{i}"
|
||||
image_data = kwargs.get(image_key)
|
||||
|
||||
if image_data is not None and len(image_data) > 0:
|
||||
saved_images = self.save_images(image_data)
|
||||
result["ui"][f"images_{i}"] = saved_images["ui"]["images"]
|
||||
|
||||
return result
|
||||
104
note_image.py
104
note_image.py
@@ -1,19 +1,14 @@
|
||||
import random
|
||||
import os
|
||||
import hashlib
|
||||
# import logging
|
||||
import numpy as np
|
||||
import torch
|
||||
from nodes import SaveImage
|
||||
import random
|
||||
from PIL import Image, ImageOps, ImageSequence
|
||||
import torch
|
||||
import folder_paths
|
||||
from PIL import Image
|
||||
from server import PromptServer
|
||||
import node_helpers
|
||||
from aiohttp import web
|
||||
|
||||
# Configure logging
|
||||
# logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
# logger = logging.getLogger("ImageNote")
|
||||
|
||||
class ImageNote(SaveImage):
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_temp_directory()
|
||||
@@ -106,3 +101,94 @@ class ImageNote(SaveImage):
|
||||
self.last_output_images = output_images
|
||||
|
||||
return super().save_images(images=output_images, prompt=prompt, extra_pnginfo=extra_pnginfo)
|
||||
class ImageNoteLoadImage:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
base_input_dir = folder_paths.get_input_directory() # Get base input directory
|
||||
input_dir = os.path.join(base_input_dir, "Bjornulf", "imagenote_images") # Specify subdirectory
|
||||
|
||||
# Create the directory if it doesn't exist
|
||||
if not os.path.exists(input_dir):
|
||||
os.makedirs(input_dir, exist_ok=True) # Create directory and parents if needed
|
||||
|
||||
# Filter for image files only
|
||||
valid_extensions = ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp')
|
||||
files = [f for f in os.listdir(input_dir) if
|
||||
os.path.isfile(os.path.join(input_dir, f)) and
|
||||
f.lower().endswith(valid_extensions)]
|
||||
|
||||
if not files:
|
||||
# Provide a default option if no files are found
|
||||
files = ["none"]
|
||||
|
||||
return {"required":
|
||||
{
|
||||
"image": (sorted(files), {"image_upload": True}),
|
||||
# "note": ("STRING", {"default": ""}), # Added multiline option FAILURE
|
||||
"note": ("STRING", {"multiline": True, "lines": 10})
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING") # Added note to return types
|
||||
RETURN_NAMES = ("image", "mask", "image_path", "note") # Added note to return names
|
||||
FUNCTION = "load_image_alpha"
|
||||
CATEGORY = "Bjornulf"
|
||||
|
||||
def load_image_alpha(self, image, note): # Added note parameter
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
|
||||
img = node_helpers.pillow(Image.open, image_path)
|
||||
|
||||
output_images = []
|
||||
output_masks = []
|
||||
w, h = None, None
|
||||
|
||||
excluded_formats = ['MPO']
|
||||
|
||||
for i in ImageSequence.Iterator(img):
|
||||
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
||||
|
||||
if i.mode == 'I':
|
||||
i = i.point(lambda i: i * (1 / 255))
|
||||
image_converted = i.convert("RGBA") # Renamed to avoid shadowing
|
||||
|
||||
if len(output_images) == 0:
|
||||
w = image_converted.size[0]
|
||||
h = image_converted.size[1]
|
||||
|
||||
if image_converted.size[0] != w or image_converted.size[1] != h:
|
||||
continue
|
||||
|
||||
image_np = np.array(image_converted).astype(np.float32) / 255.0 # Renamed to avoid shadowing
|
||||
image_tensor = torch.from_numpy(image_np)[None,] # Renamed to avoid shadowing
|
||||
if 'A' in i.getbands():
|
||||
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
||||
mask = 1. - torch.from_numpy(mask)
|
||||
else:
|
||||
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
|
||||
output_images.append(image_tensor) # Renamed to avoid shadowing
|
||||
output_masks.append(mask.unsqueeze(0))
|
||||
|
||||
if len(output_images) > 1 and img.format not in excluded_formats:
|
||||
output_image = torch.cat(output_images, dim=0)
|
||||
output_mask = torch.cat(output_masks, dim=0)
|
||||
else:
|
||||
output_image = output_images[0]
|
||||
output_mask = output_masks[0]
|
||||
|
||||
return (output_image, output_mask, image_path, note) # Added note to return tuple
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, image, note): # Added note to IS_CHANGED
|
||||
image_path = folder_paths.get_annotated_filepath(image)
|
||||
m = hashlib.sha256()
|
||||
with open(image_path, 'rb') as f:
|
||||
m.update(f.read())
|
||||
return m.digest().hex() + str(note) # Include note in hash
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, image):
|
||||
if not folder_paths.exists_annotated_filepath(image):
|
||||
return "Invalid image file: {}".format(image)
|
||||
|
||||
return True
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "bjornulf_custom_nodes"
|
||||
description = "128 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 Kokoro, etc..."
|
||||
version = "0.70"
|
||||
description = "133 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 Kokoro, etc..."
|
||||
version = "0.71"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import os
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import json
|
||||
from PIL.PngImagePlugin import PngInfo
|
||||
import folder_paths
|
||||
from nodes import SaveImage
|
||||
|
||||
class SaveImageToFolder:
|
||||
class SaveImageToFolder(SaveImage):
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
@@ -15,40 +13,20 @@ class SaveImageToFolder:
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
FUNCTION = "save_image_to_folder"
|
||||
RETURN_TYPES = ()
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "save_images"
|
||||
CATEGORY = "Bjornulf"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def save_image_to_folder(self, images, folder_name, prompt=None, extra_pnginfo=None):
|
||||
output_dir = os.path.join("./output", folder_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
results = []
|
||||
def save_images(self, images, folder_name, prompt=None, extra_pnginfo=None):
|
||||
# Create the custom folder within the output directory
|
||||
custom_folder = os.path.join(folder_paths.get_output_directory(), folder_name)
|
||||
os.makedirs(custom_folder, exist_ok=True)
|
||||
|
||||
# Find the highest existing file number
|
||||
existing_files = [f for f in os.listdir(output_dir) if f.endswith('.png') and f[:5].isdigit()]
|
||||
if existing_files:
|
||||
highest_num = max(int(f[:5]) for f in existing_files)
|
||||
counter = highest_num + 1
|
||||
else:
|
||||
counter = 1
|
||||
|
||||
for image in images:
|
||||
i = 255. * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
|
||||
|
||||
metadata = PngInfo()
|
||||
if prompt is not None:
|
||||
metadata.add_text("prompt", json.dumps(prompt))
|
||||
if extra_pnginfo is not None:
|
||||
for k, v in extra_pnginfo.items():
|
||||
metadata.add_text(k, json.dumps(v))
|
||||
|
||||
filename = os.path.join(output_dir, f"{counter:05d}.png")
|
||||
img.save(filename, format="PNG", pnginfo=metadata)
|
||||
print(f"Image saved as: {filename}")
|
||||
results.append({"filename": filename})
|
||||
counter += 1
|
||||
|
||||
return {"ui": {"images": [{"filename": filename, "type": "output"}]}}
|
||||
# Call the parent's save_images with filename_prefix set to "folder_name/"
|
||||
# This will make the parent class save to the custom folder
|
||||
return super().save_images(
|
||||
images=images,
|
||||
filename_prefix=f"{folder_name}/_",
|
||||
prompt=prompt,
|
||||
extra_pnginfo=extra_pnginfo
|
||||
)
|
||||
BIN
screenshots/four_preview.png
Normal file
BIN
screenshots/four_preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 803 KiB |
BIN
screenshots/four_preview_zoom.png
Normal file
BIN
screenshots/four_preview_zoom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 802 KiB |
BIN
screenshots/note_load_image.png
Normal file
BIN
screenshots/note_load_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
screenshots/text_split_10.png
Normal file
BIN
screenshots/text_split_10.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
screenshots/text_to_variable.png
Normal file
BIN
screenshots/text_to_variable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
screenshots/write_pick_me_chain.png
Normal file
BIN
screenshots/write_pick_me_chain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -4,6 +4,7 @@ import os
|
||||
import numpy as np
|
||||
import tempfile
|
||||
import wave
|
||||
import subprocess # Added for ffmpeg
|
||||
|
||||
try:
|
||||
import faster_whisper
|
||||
@@ -25,6 +26,7 @@ class SpeechToText:
|
||||
"optional": {
|
||||
"AUDIO": ("AUDIO",),
|
||||
"audio_path": ("STRING", {"default": None, "forceInput": True}),
|
||||
"video_path": ("STRING", {"default": None, "forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,26 +37,25 @@ class SpeechToText:
|
||||
|
||||
def tensor_to_wav(self, audio_tensor, sample_rate):
|
||||
"""Convert audio tensor to temporary WAV file"""
|
||||
# Convert tensor to numpy array
|
||||
audio_data = audio_tensor.squeeze().numpy()
|
||||
|
||||
# Create temporary file
|
||||
if audio_data.ndim == 2:
|
||||
audio_data = np.mean(audio_data, axis=0)
|
||||
elif audio_data.ndim > 2:
|
||||
raise ValueError(f"Unsupported audio tensor shape: {audio_data.shape}")
|
||||
|
||||
temp_file = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
|
||||
|
||||
# Write WAV file
|
||||
with wave.open(temp_file.name, 'wb') as wav_file:
|
||||
wav_file.setnchannels(1) # Mono audio
|
||||
wav_file.setsampwidth(2) # 2 bytes per sample
|
||||
wav_file.setnchannels(1)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(sample_rate)
|
||||
|
||||
# Convert float32 to int16
|
||||
audio_data = (audio_data * 32767).astype(np.int16)
|
||||
wav_file.writeframes(audio_data.tobytes())
|
||||
|
||||
return temp_file.name
|
||||
|
||||
def load_local_model(self, model_size):
|
||||
"""Load the local Whisper model if not already loaded"""
|
||||
if not WHISPER_AVAILABLE:
|
||||
return False, "faster-whisper not installed. Install with: pip install faster-whisper"
|
||||
|
||||
@@ -68,7 +69,6 @@ class SpeechToText:
|
||||
return False, f"Error loading model: {str(e)}"
|
||||
|
||||
def transcribe_local(self, audio_path, model_size):
|
||||
"""Transcribe audio using local Whisper model"""
|
||||
success, message = self.load_local_model(model_size)
|
||||
if not success:
|
||||
return False, message, None
|
||||
@@ -83,23 +83,47 @@ class SpeechToText:
|
||||
except Exception as e:
|
||||
return False, f"Error during local transcription: {str(e)}", None
|
||||
|
||||
def transcribe_audio(self, model_size, AUDIO=None, audio_path=None):
|
||||
def transcribe_audio(self, model_size, AUDIO=None, audio_path=None, video_path=None):
|
||||
transcript = "No valid audio input provided"
|
||||
detected_language = ""
|
||||
temp_wav_path = None
|
||||
temp_audio_path = None
|
||||
|
||||
try:
|
||||
# Determine which audio source to use
|
||||
if AUDIO is not None:
|
||||
# Convert tensor audio data to WAV file
|
||||
# Check video input first
|
||||
if video_path and os.path.exists(video_path):
|
||||
try:
|
||||
# Create temp file for extracted audio
|
||||
temp_audio = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
|
||||
temp_audio.close()
|
||||
temp_audio_path = temp_audio.name
|
||||
|
||||
# FFmpeg command to extract audio
|
||||
command = [
|
||||
'ffmpeg',
|
||||
'-i', video_path,
|
||||
'-vn',
|
||||
'-acodec', 'pcm_s16le',
|
||||
'-ar', '16000',
|
||||
'-ac', '1',
|
||||
'-y',
|
||||
temp_audio_path
|
||||
]
|
||||
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
audio_to_process = temp_audio_path
|
||||
except subprocess.CalledProcessError as e:
|
||||
return (f"FFmpeg error: {e.stderr.decode()}", "", "")
|
||||
except Exception as e:
|
||||
return (f"Error extracting audio: {str(e)}", "", "")
|
||||
elif AUDIO is not None:
|
||||
waveform = AUDIO['waveform']
|
||||
sample_rate = AUDIO['sample_rate']
|
||||
temp_wav_path = self.tensor_to_wav(waveform, sample_rate)
|
||||
audio_to_process = temp_wav_path
|
||||
elif audio_path is not None and os.path.exists(audio_path):
|
||||
elif audio_path and os.path.exists(audio_path):
|
||||
audio_to_process = audio_path
|
||||
else:
|
||||
return ("No valid audio input provided", "")
|
||||
return ("No valid audio input provided", "", "")
|
||||
|
||||
if audio_to_process:
|
||||
success, result, lang = self.transcribe_local(audio_to_process, model_size)
|
||||
@@ -107,11 +131,12 @@ class SpeechToText:
|
||||
detected_language = lang if success else ""
|
||||
|
||||
finally:
|
||||
# Clean up temporary file if it was created
|
||||
# Cleanup temporary files
|
||||
if temp_wav_path and os.path.exists(temp_wav_path):
|
||||
os.unlink(temp_wav_path)
|
||||
if temp_audio_path and os.path.exists(temp_audio_path):
|
||||
os.unlink(temp_audio_path)
|
||||
|
||||
#Create detected_language_name based on detected_language, en = English, es = Spanish, fr = French, de = German, etc...
|
||||
language_map = {
|
||||
"ar": "Arabic", "cs": "Czech", "de": "German", "en": "English",
|
||||
"es": "Spanish", "fr": "French", "hi": "Hindi", "hu": "Hungarian",
|
||||
@@ -121,4 +146,4 @@ class SpeechToText:
|
||||
}
|
||||
detected_language_name = language_map.get(detected_language, "Unknown")
|
||||
|
||||
return (transcript, detected_language,detected_language_name)
|
||||
return (transcript, detected_language, detected_language_name)
|
||||
@@ -45,5 +45,55 @@ class TextSplitin5:
|
||||
# If no more parts, append empty string
|
||||
result.append("")
|
||||
|
||||
# Convert to tuple and return all 5 parts
|
||||
return tuple(result)
|
||||
|
||||
class TextSplitin10:
|
||||
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","STRING", "STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("part1", "part2", "part3", "part4", "part5", "part6", "part7", "part8", "part9", "part10")
|
||||
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(10):
|
||||
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)
|
||||
14
text_to_variable.py
Normal file
14
text_to_variable.py
Normal file
@@ -0,0 +1,14 @@
|
||||
class TextToVariable:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"variable_name": ("STRING", {"default": "variable_name"}),
|
||||
"text_value": ("STRING", {"forceInput": True})}}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "process"
|
||||
CATEGORY = "Custom"
|
||||
|
||||
def process(self, variable_name, text_value):
|
||||
text_value = text_value.replace("\n", ";")
|
||||
output_string = f"{variable_name} = {text_value}"
|
||||
return (output_string,)
|
||||
210
web/js/images_compare.js
Normal file
210
web/js/images_compare.js
Normal file
@@ -0,0 +1,210 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
function imageDataToUrl(data) {
|
||||
return api.apiURL(
|
||||
`/view?filename=${encodeURIComponent(data.filename)}&type=${data.type}&subfolder=${data.subfolder}${app.getPreviewFormatParam()}`
|
||||
);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Bjornulf.FourImageViewer",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "Bjornulf_FourImageViewer") return;
|
||||
|
||||
const marginTop = 90;
|
||||
const verticalOffset = -30;
|
||||
const minSize = 256; // Minimum size for the node
|
||||
const maxSize = 2048; // Maximum size for the node
|
||||
node.size = [512, 512 + marginTop];
|
||||
node.images = new Array(4).fill(null);
|
||||
const padding = 10;
|
||||
|
||||
// Add resize handles
|
||||
node.flags |= LiteGraph.RESIZABLE;
|
||||
|
||||
node.onResize = function(size) {
|
||||
// Ensure square aspect ratio (excluding marginTop)
|
||||
const minDimension = Math.max(minSize, Math.min(size[0], size[1] - marginTop));
|
||||
const maxDimension = Math.min(maxSize, minDimension);
|
||||
size[0] = maxDimension;
|
||||
size[1] = maxDimension + marginTop;
|
||||
|
||||
// Update slider positions proportionally
|
||||
const fullImgWidth = size[0] - 3 * padding;
|
||||
const fullImgHeight = size[1] - padding - marginTop;
|
||||
|
||||
// Only update sliders if they exist (node has been initialized)
|
||||
if (node.hasOwnProperty('sliderX')) {
|
||||
const oldWidth = node.size[0] - 3 * padding;
|
||||
const oldHeight = node.size[1] - padding - marginTop;
|
||||
|
||||
// Calculate relative positions (0 to 1)
|
||||
const relativeX = (node.sliderX - padding) / oldWidth;
|
||||
const relativeY = (node.sliderY - marginTop) / oldHeight;
|
||||
|
||||
// Update slider positions
|
||||
node.sliderX = padding + (fullImgWidth * relativeX);
|
||||
node.sliderY = marginTop + (fullImgHeight * relativeY);
|
||||
} else {
|
||||
// Initial slider positions
|
||||
node.sliderX = padding + fullImgWidth / 2;
|
||||
node.sliderY = marginTop + fullImgHeight / 2;
|
||||
}
|
||||
|
||||
node.size = size;
|
||||
return size;
|
||||
};
|
||||
|
||||
// Full area where images get drawn
|
||||
const fullImgWidth = node.size[0] - 3 * padding;
|
||||
const fullImgHeight = node.size[1] - padding - marginTop;
|
||||
node.sliderX = padding + fullImgWidth / 2;
|
||||
node.sliderY = marginTop + fullImgHeight / 2;
|
||||
|
||||
node.onMouseDown = function(e) {
|
||||
const rect = node.getBounding();
|
||||
const [clickX, clickY] = [
|
||||
e.canvasX - rect[0],
|
||||
e.canvasY - rect[1] + verticalOffset
|
||||
];
|
||||
|
||||
const imgWidth = rect[2] - 3 * padding;
|
||||
const imgHeight = rect[3] - padding - marginTop;
|
||||
const xStart = padding;
|
||||
const xEnd = xStart + imgWidth;
|
||||
const yStart = marginTop;
|
||||
const yEnd = yStart + imgHeight;
|
||||
|
||||
if (clickX >= xStart && clickX <= xEnd && clickY >= yStart && clickY <= yEnd) {
|
||||
const hasImage2 = node.images[1] !== null;
|
||||
const hasImage3 = node.images[2] !== null;
|
||||
const hasImage4 = node.images[3] !== null;
|
||||
if (hasImage2 && !hasImage3 && !hasImage4) {
|
||||
node.sliderY = yEnd;
|
||||
}
|
||||
node.sliderX = Math.max(xStart, Math.min(clickX, xEnd));
|
||||
node.sliderY = hasImage3 || hasImage4
|
||||
? Math.max(yStart, Math.min(clickY, yEnd))
|
||||
: yEnd;
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
node.onExecuted = async function(message) {
|
||||
node.images = new Array(4).fill(null);
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const images = message[`images_${i}`] || [];
|
||||
if (images.length) {
|
||||
const imgData = images[0];
|
||||
const img = new Image();
|
||||
img.src = imageDataToUrl(imgData);
|
||||
await new Promise(resolve => (img.onload = img.onerror = resolve));
|
||||
node.images[i - 1] = img;
|
||||
}
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
node.onDrawForeground = function(ctx) {
|
||||
const padding = 10;
|
||||
const xStart = padding;
|
||||
const xEnd = xStart + (node.size[0] - 3 * padding);
|
||||
const yStart = marginTop;
|
||||
const yEnd = yStart + (node.size[1] - padding - marginTop);
|
||||
const fullImgWidth = node.size[0] - 3 * padding;
|
||||
const fullImgHeight = node.size[1] - padding - marginTop;
|
||||
|
||||
function getFittedDestRect(dx, dy, dWidth, dHeight, targetRatio) {
|
||||
let newWidth = dWidth;
|
||||
let newHeight = dWidth / targetRatio;
|
||||
if (newHeight > dHeight) {
|
||||
newHeight = dHeight;
|
||||
newWidth = dHeight * targetRatio;
|
||||
}
|
||||
const offsetX = dx + (dWidth - newWidth) / 2;
|
||||
const offsetY = dy + (dHeight - newHeight) / 2;
|
||||
return [offsetX, offsetY, newWidth, newHeight];
|
||||
}
|
||||
|
||||
function drawCroppedImage(img, dx, dy, dWidth, dHeight) {
|
||||
if (!img) return;
|
||||
let targetRatio = dWidth / dHeight;
|
||||
if (node.images[0] && node.images[0].naturalWidth && node.images[0].naturalHeight) {
|
||||
targetRatio = node.images[0].naturalWidth / node.images[0].naturalHeight;
|
||||
}
|
||||
|
||||
const [ndx, ndy, ndWidth, ndHeight] = getFittedDestRect(dx, dy, dWidth, dHeight, targetRatio);
|
||||
|
||||
const imgRatio = img.naturalWidth / img.naturalHeight;
|
||||
let sx = 0, sy = 0, sWidth = img.naturalWidth, sHeight = img.naturalHeight;
|
||||
if (imgRatio > targetRatio) {
|
||||
sWidth = img.naturalHeight * targetRatio;
|
||||
sx = (img.naturalWidth - sWidth) / 2;
|
||||
} else if (imgRatio < targetRatio) {
|
||||
sHeight = img.naturalWidth / targetRatio;
|
||||
sy = (img.naturalHeight - sHeight) / 2;
|
||||
}
|
||||
ctx.drawImage(img, sx, sy, sWidth, sHeight, ndx, ndy, ndWidth, ndHeight);
|
||||
}
|
||||
|
||||
const connectedImages = node.images.slice(1).filter(img => img !== null).length;
|
||||
|
||||
if (connectedImages === 0) {
|
||||
if (node.images[0]) {
|
||||
drawCroppedImage(node.images[0], xStart, yStart, fullImgWidth, fullImgHeight);
|
||||
}
|
||||
} else if (connectedImages === 1 && node.images[1]) {
|
||||
const splitX = node.sliderX;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(xStart, yStart, splitX - xStart, fullImgHeight);
|
||||
ctx.clip();
|
||||
drawCroppedImage(node.images[0], xStart, yStart, fullImgWidth, fullImgHeight);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(splitX, yStart, xEnd - splitX, fullImgHeight);
|
||||
ctx.clip();
|
||||
drawCroppedImage(node.images[1], xStart, yStart, fullImgWidth, fullImgHeight);
|
||||
ctx.restore();
|
||||
} else {
|
||||
const drawQuadrant = (imgIndex, clipX, clipY, clipW, clipH) => {
|
||||
if (!node.images[imgIndex]) return;
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.rect(clipX, clipY, clipW, clipH);
|
||||
ctx.clip();
|
||||
drawCroppedImage(node.images[imgIndex], xStart, yStart, fullImgWidth, fullImgHeight);
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
drawQuadrant(0, xStart, yStart, node.sliderX - xStart, node.sliderY - yStart);
|
||||
drawQuadrant(1, node.sliderX, yStart, xEnd - node.sliderX, node.sliderY - yStart);
|
||||
|
||||
if (node.images[3] === null) {
|
||||
drawQuadrant(2, xStart, node.sliderY, xEnd - xStart, yEnd - node.sliderY);
|
||||
} else {
|
||||
drawQuadrant(2, xStart, node.sliderY, node.sliderX - xStart, yEnd - node.sliderY);
|
||||
drawQuadrant(3, node.sliderX, node.sliderY, xEnd - node.sliderX, yEnd - node.sliderY);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.strokeStyle = "#FFF";
|
||||
ctx.lineWidth = 1;
|
||||
if (connectedImages > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(node.sliderX, yStart);
|
||||
ctx.lineTo(node.sliderX, yEnd);
|
||||
if (connectedImages >= 2) {
|
||||
ctx.moveTo(xStart, node.sliderY);
|
||||
ctx.lineTo(xEnd, node.sliderY);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
80
web/js/note_image.js
Normal file
80
web/js/note_image.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
app.registerExtension({
|
||||
name: "Bjornulf.ImageNoteLoadImage",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass !== "Bjornulf_ImageNoteLoadImage") return;
|
||||
|
||||
setTimeout(() => {
|
||||
// Update widget positions
|
||||
node.onResize(node.size);
|
||||
|
||||
// Refresh all widgets
|
||||
node.widgets.forEach(w => {
|
||||
if (w.onShow?.(true)) {
|
||||
w.onShow?.(false);
|
||||
}
|
||||
});
|
||||
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// app.registerExtension({
|
||||
// name: "Bjornulf.ImageNote",
|
||||
// async nodeCreated(node) {
|
||||
// if (node.comfyClass !== "Bjornulf_ImageNote") return;
|
||||
|
||||
// // Add Save Note button
|
||||
// node.addWidget("button", "Save Note", null, () => {
|
||||
// const imagePathWidget = node.widgets.find(w => w.name === "image_path");
|
||||
// const noteTextWidget = node.widgets.find(w => w.name === "note_text");
|
||||
|
||||
// if (!imagePathWidget?.value) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// fetch("/save_note", {
|
||||
// method: "POST",
|
||||
// body: JSON.stringify({
|
||||
// image_path: imagePathWidget.value,
|
||||
// note_text: noteTextWidget?.value || ""
|
||||
// }),
|
||||
// headers: { "Content-Type": "application/json" }
|
||||
// })
|
||||
// .then(response => response.json())
|
||||
// .catch(error => {
|
||||
// console.error("Error saving note:", error);
|
||||
// });
|
||||
// });
|
||||
|
||||
// // Add Load Note button
|
||||
// node.addWidget("button", "Load Note", null, () => {
|
||||
// const imagePathWidget = node.widgets.find(w => w.name === "image_path");
|
||||
|
||||
// if (!imagePathWidget?.value) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// fetch("/load_note", {
|
||||
// method: "POST",
|
||||
// body: JSON.stringify({ image_path: imagePathWidget.value }),
|
||||
// headers: { "Content-Type": "application/json" }
|
||||
// })
|
||||
// .then(response => response.json())
|
||||
// .then(data => {
|
||||
// if (data.success) {
|
||||
// const noteTextWidget = node.widgets.find(w => w.name === "note_text");
|
||||
// if (noteTextWidget) {
|
||||
// noteTextWidget.value = data.note_text;
|
||||
// // Trigger widget changed event to update UI
|
||||
// app.graph.setDirtyCanvas(true);
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error("Error loading note:", error);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
172
web/js/write_pickme_chain.js
Normal file
172
web/js/write_pickme_chain.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
|
||||
// Helper function to clean up widget DOM elements
|
||||
function cleanupWidgetDOM(widget) {
|
||||
if (widget && widget.inputEl) {
|
||||
if (widget.inputEl.parentElement) {
|
||||
widget.inputEl.parentElement.remove();
|
||||
} else {
|
||||
widget.inputEl.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getChainNodes(startNode) {
|
||||
const nodes = [];
|
||||
let currentNode = startNode;
|
||||
|
||||
// First traverse upstream to find the root node
|
||||
while (true) {
|
||||
const input = currentNode.inputs.find(i => i.name === "pickme_chain");
|
||||
if (input?.link) {
|
||||
const link = app.graph.links[input.link];
|
||||
const prevNode = app.graph.getNodeById(link.origin_id);
|
||||
if (prevNode?.comfyClass === "Bjornulf_WriteTextPickMeChain") {
|
||||
currentNode = prevNode;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Now traverse downstream from root
|
||||
while (currentNode) {
|
||||
nodes.push(currentNode);
|
||||
const output = currentNode.outputs.find(o => o.name === "chain_text");
|
||||
if (output?.links) {
|
||||
let nextNode = null;
|
||||
for (const linkId of output.links) {
|
||||
const link = app.graph.links[linkId];
|
||||
const targetNode = app.graph.getNodeById(link.target_id);
|
||||
if (targetNode?.comfyClass === "Bjornulf_WriteTextPickMeChain") {
|
||||
nextNode = targetNode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentNode = nextNode;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function pickNode(node) {
|
||||
const chainNodes = getChainNodes(node);
|
||||
chainNodes.forEach(n => {
|
||||
const pickedWidget = n.widgets.find(w => w.name === "picked");
|
||||
if (pickedWidget) {
|
||||
const isPicked = n === node;
|
||||
pickedWidget.value = isPicked;
|
||||
n.color = isPicked ? "#006400" : "";
|
||||
}
|
||||
});
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
|
||||
// Rest of the code remains the same as previous working version
|
||||
|
||||
function findAndPickNext(removedNode) {
|
||||
const chainNodes = getChainNodes(removedNode);
|
||||
const remaining = chainNodes.filter(n => n.id !== removedNode.id);
|
||||
if (remaining.length) pickNode(remaining[0]);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Bjornulf.WriteTextPickMeChain",
|
||||
async nodeCreated(node) {
|
||||
if (node.comfyClass === "Bjornulf_WriteTextPickMeChain") {
|
||||
// Store original onRemoved if it exists
|
||||
const origOnRemoved = node.onRemoved;
|
||||
// Create widgets in specific order to maintain layout
|
||||
// const textWidget = node.widgets.find(w => w.name === "text");
|
||||
// if (textWidget) {
|
||||
// textWidget.computeSize = function() {
|
||||
// return [node.size[0] - 20, 150];
|
||||
// };
|
||||
// }
|
||||
|
||||
// Handle picked widget
|
||||
let pickedWidget = node.widgets.find(w => w.name === "picked");
|
||||
if (!pickedWidget) {
|
||||
pickedWidget = node.addWidget("BOOLEAN", "picked", false, null);
|
||||
}
|
||||
pickedWidget.visible = false;
|
||||
|
||||
// Add button after textarea
|
||||
const buttonWidget = node.addWidget("button", "PICK ME", null, () => pickNode(node));
|
||||
buttonWidget.computeSize = function() {
|
||||
return [node.size[0] - 20, 30];
|
||||
};
|
||||
|
||||
// Set initial node size
|
||||
// node.size = [node.size[0], 200];
|
||||
// node.size = [200, 200];
|
||||
setTimeout(() => {
|
||||
// Update widget positions
|
||||
node.onResize(node.size);
|
||||
|
||||
// Refresh all widgets
|
||||
node.widgets.forEach(w => {
|
||||
if (w.onShow?.(true)) {
|
||||
w.onShow?.(false);
|
||||
}
|
||||
});
|
||||
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}, 10);
|
||||
|
||||
// Enhanced cleanup on node removal
|
||||
node.onRemoved = function() {
|
||||
// Call original onRemoved if it exists
|
||||
if (origOnRemoved) {
|
||||
origOnRemoved.call(this);
|
||||
}
|
||||
|
||||
// Handle chain updates
|
||||
if (this.widgets.find(w => w.name === "picked")?.value) {
|
||||
findAndPickNext(this);
|
||||
}
|
||||
|
||||
// Clean up all widgets
|
||||
for (const widget of this.widgets) {
|
||||
cleanupWidgetDOM(widget);
|
||||
}
|
||||
|
||||
// Force DOM cleanup and canvas update
|
||||
if (this.domElement) {
|
||||
this.domElement.remove();
|
||||
}
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const updateColors = () => {
|
||||
const picked = node.widgets.find(w => w.name === "picked")?.value;
|
||||
node.color = picked ? "#006400" : "";
|
||||
};
|
||||
|
||||
const origSetNodeState = node.setNodeState;
|
||||
node.setNodeState = function(state) {
|
||||
origSetNodeState?.apply(this, arguments);
|
||||
if (state.picked !== undefined) {
|
||||
const widget = this.widgets.find(w => w.name === "picked");
|
||||
if (widget) widget.value = state.picked;
|
||||
}
|
||||
updateColors();
|
||||
};
|
||||
|
||||
const origGetNodeState = node.getNodeState;
|
||||
node.getNodeState = function() {
|
||||
const state = origGetNodeState?.apply(this, arguments) || {};
|
||||
state.picked = this.widgets.find(w => w.name === "picked")?.value ?? false;
|
||||
return state;
|
||||
};
|
||||
|
||||
// Force initial layout update
|
||||
app.graph.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
22
write_pickme_chain.py
Normal file
22
write_pickme_chain.py
Normal file
@@ -0,0 +1,22 @@
|
||||
class WriteTextPickMeChain:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"picked": ("BOOLEAN", {"default": False}),
|
||||
"text": ("STRING", {"multiline": True, "lines": 10})
|
||||
},
|
||||
"optional": {
|
||||
"pickme_chain": ("STRING", {"forceInput": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING")
|
||||
RETURN_NAMES = ("text", "chain_text")
|
||||
FUNCTION = "write_text"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "Bjornulf"
|
||||
|
||||
def write_text(self, text, picked, pickme_chain="", **kwargs):
|
||||
chain_output = text if picked else pickme_chain
|
||||
return (text, chain_output)
|
||||
Reference in New Issue
Block a user