Character selector update
This commit is contained in:
+10
-94
@@ -1,99 +1,15 @@
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
import nodes
|
||||
# Register JS web dir for custom hook
|
||||
WEB_DIRECTORY = os.path.join(os.path.dirname(__file__), "js")
|
||||
nodes.EXTENSION_WEB_DIRS["ComfyUI_AnimeCharacterSelect"] = WEB_DIRECTORY
|
||||
|
||||
class EnhancedCharacterPromptNode:
|
||||
"""ComfyUI node: select character + action, show preview image, editable prompt (live insert), output prompt + conditioning"""
|
||||
|
||||
# Load all character/action data on class load
|
||||
CHARACTER_JSON_FILES = [
|
||||
os.path.join(os.path.dirname(__file__), f"output_{i}.json") for i in range(1, 12)
|
||||
]
|
||||
ACTION_JSON = os.path.join(os.path.dirname(__file__), "action.json")
|
||||
|
||||
char_data = []
|
||||
action_data = {}
|
||||
CHARACTERS = []
|
||||
ACTIONS = []
|
||||
|
||||
try:
|
||||
for path in CHARACTER_JSON_FILES:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
char_data.extend(json.load(f))
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error loading {path}: {e}")
|
||||
|
||||
try:
|
||||
with open(ACTION_JSON, "r", encoding="utf-8") as f:
|
||||
action_data = json.load(f)
|
||||
except Exception as e:
|
||||
print("❌ Error loading action.json:", e)
|
||||
|
||||
CHARACTERS = [list(entry.keys())[0] for entry in char_data if isinstance(entry, dict) and len(entry) >= 1]
|
||||
ACTIONS = list(action_data.keys())
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "dynamicPrompts": False, "default": ""}),
|
||||
"character": (["Select character..."] + cls.CHARACTERS,),
|
||||
"action": (["Select action..."] + cls.ACTIONS,),
|
||||
"clip": ("CLIP",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "IMAGE", "CONDITIONING")
|
||||
RETURN_NAMES = ("prompt", "preview_image", "conditioning")
|
||||
FUNCTION = "build_prompt"
|
||||
CATEGORY = "Prompting/Anime Character"
|
||||
|
||||
def build_prompt(self, prompt, character, action, clip):
|
||||
# Lookup preview image for character
|
||||
preview_image = None
|
||||
if character and character != "Select character...":
|
||||
for entry in self.char_data:
|
||||
if isinstance(entry, dict) and character in entry:
|
||||
value = entry[character]
|
||||
preview_data = value if isinstance(value, str) and value.startswith("data:image") else entry.get("preview", "")
|
||||
if preview_data and preview_data.startswith("data:image"):
|
||||
try:
|
||||
base64_data = preview_data.split("base64,", 1)[1]
|
||||
preview_image = self.decode_base64_to_image(base64_data)
|
||||
except Exception as e:
|
||||
print(f"⚠️ Base64 decode failed for {character}: {e}")
|
||||
break
|
||||
|
||||
# Prompt is fully user-editable: whatever is in the text field is what you get!
|
||||
final_prompt = prompt.strip()
|
||||
conditioning_output = []
|
||||
if clip and final_prompt:
|
||||
try:
|
||||
cross_attn = clip.encode(final_prompt)
|
||||
conditioning_output = [(cross_attn, {})]
|
||||
except Exception as e:
|
||||
print(f"⚠️ CLIP encode failed: {e}")
|
||||
|
||||
return (final_prompt, preview_image, conditioning_output)
|
||||
|
||||
def decode_base64_to_image(self, base64_str):
|
||||
data = base64.b64decode(base64_str)
|
||||
try:
|
||||
from comfy.utils import pil_to_tensor
|
||||
return pil_to_tensor(Image.open(BytesIO(data)).convert("RGB"))
|
||||
except Exception as e:
|
||||
raise ValueError("Failed to decode base64 image") from e
|
||||
from .action_prompt_node import ActionPromptNode
|
||||
from .character_prompt_node import CharacterPromptNode
|
||||
from .combine_prompt_strings import CombinePromptStringsNode
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"EnhancedCharacterPromptNode": EnhancedCharacterPromptNode,
|
||||
"ActionPromptNode": ActionPromptNode,
|
||||
"CharacterPromptNode": CharacterPromptNode,
|
||||
"CombinePromptStringsNode": CombinePromptStringsNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"EnhancedCharacterPromptNode": "Character + Action Prompt (Live Insert)",
|
||||
"ActionPromptNode": "Action Prompt (Editable)",
|
||||
"CharacterPromptNode": "Character Prompt + Image",
|
||||
"CombinePromptStringsNode": "Combine Prompt Strings",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import os
|
||||
import json
|
||||
import nodes
|
||||
|
||||
nodes.EXTENSION_WEB_DIRS["ComfyUI_AnimeCharacterSelect"] = os.path.join(os.path.dirname(__file__), "js")
|
||||
|
||||
class ActionPromptNode:
|
||||
ACTION_JSON = os.path.join(os.path.dirname(__file__), "action.json")
|
||||
ACTION_LABELS = []
|
||||
ACTION_PROMPTS = {}
|
||||
|
||||
try:
|
||||
with open(ACTION_JSON, "r", encoding="utf-8") as f:
|
||||
_data = json.load(f)
|
||||
ACTION_LABELS = list(_data.keys())
|
||||
ACTION_PROMPTS = _data
|
||||
except Exception as e:
|
||||
print("Error loading action.json:", e)
|
||||
ACTION_LABELS = ["No actions found"]
|
||||
ACTION_PROMPTS = {}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"action_selector": (cls.ACTION_LABELS,),
|
||||
"action_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("action_prompt",)
|
||||
FUNCTION = "set_prompt"
|
||||
CATEGORY = "Prompting/Anime"
|
||||
|
||||
def set_prompt(self, action_selector, action_prompt):
|
||||
# Always output the editable prompt field, not the selector
|
||||
return (action_prompt,)
|
||||
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
class CharacterPromptNode:
|
||||
"""
|
||||
Node: Character selection for anime prompts.
|
||||
Non-editable output string, includes preview image (base64 in output_X.json).
|
||||
"""
|
||||
|
||||
CHARACTER_JSON_FILES = [
|
||||
os.path.join(os.path.dirname(__file__), f"output_{i}.json") for i in range(1, 13)
|
||||
]
|
||||
|
||||
char_data = []
|
||||
CHARACTERS = []
|
||||
|
||||
try:
|
||||
for path in CHARACTER_JSON_FILES:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
char_data.extend(json.load(f))
|
||||
except Exception as e:
|
||||
print(f"[CharacterPromptNode] ⚠️ Error loading {path}: {e}")
|
||||
|
||||
CHARACTERS = [list(entry.keys())[0] for entry in char_data if isinstance(entry, dict) and len(entry) >= 1]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"character": (cls.CHARACTERS,),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "IMAGE")
|
||||
RETURN_NAMES = ("character_prompt", "preview_image")
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Prompting/Anime Character"
|
||||
|
||||
def run(self, character):
|
||||
char_prompt = character
|
||||
preview_image = None
|
||||
# Find preview
|
||||
for entry in self.char_data:
|
||||
if isinstance(entry, dict) and character in entry:
|
||||
value = entry[character]
|
||||
preview_data = value if isinstance(value, str) and value.startswith("data:image") else entry.get("preview", "")
|
||||
if isinstance(preview_data, str) and preview_data.startswith("data:image"):
|
||||
try:
|
||||
base64_data = preview_data.split("base64,", 1)[1]
|
||||
preview_image = self.decode_base64_to_image(base64_data)
|
||||
except Exception as e:
|
||||
print(f"[CharacterPromptNode] ⚠️ Base64 decode failed for {character}: {e}")
|
||||
break
|
||||
return (char_prompt, preview_image)
|
||||
|
||||
def decode_base64_to_image(self, base64_str):
|
||||
data = base64.b64decode(base64_str)
|
||||
try:
|
||||
from comfy.utils import pil_to_tensor
|
||||
return pil_to_tensor(Image.open(BytesIO(data)).convert("RGB"))
|
||||
except Exception as e:
|
||||
raise ValueError("Failed to decode base64 image")
|
||||
@@ -0,0 +1,27 @@
|
||||
class CombinePromptStringsNode:
|
||||
"""
|
||||
Node: Combines character prompt, action prompt, and extra prompt into one string.
|
||||
Connect the outputs of CharacterPromptNode and ActionPromptNode here, plus any extra prompt string.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"character_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"action_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"extra_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "Prompting/Anime Character"
|
||||
|
||||
def run(self, character_prompt, action_prompt, extra_prompt):
|
||||
parts = [p.strip() for p in [character_prompt, action_prompt, extra_prompt] if p and p.strip()]
|
||||
prompt = ", ".join(parts)
|
||||
return (prompt,)
|
||||
|
||||
# Usage: Connect the outputs of CharacterPromptNode and ActionPromptNode, plus any extra string (or leave empty), to this node for your final prompt.
|
||||
@@ -0,0 +1,40 @@
|
||||
app.registerExtension({
|
||||
name: "ComfyUI_AnimeCharacterSelect_action_prompt",
|
||||
beforeWidgetRender: async ({ widget, node }) => {
|
||||
// Only on first render of action_prompt textarea
|
||||
if (widget.name !== "action_prompt") return;
|
||||
const textarea = widget.inputEl;
|
||||
if (!textarea) return;
|
||||
|
||||
// Find dropdown widget for action_selector
|
||||
const dropdown = node.widgets.find(w => w.name === "action_selector");
|
||||
if (!dropdown || !dropdown.inputEl) return;
|
||||
|
||||
// Build a lookup table (Action label => Prompt)
|
||||
let promptMap = {};
|
||||
try {
|
||||
// Try to read the map from the backend (Comfy passes extra_data sometimes)
|
||||
if (dropdown.options && dropdown.options.length > 0) {
|
||||
dropdown.options.forEach((opt, idx) => {
|
||||
// Only works if promptMap is sent via extra_data or a hidden input.
|
||||
});
|
||||
}
|
||||
} catch (e) { /* fallback below */ }
|
||||
|
||||
// Hard-coded for testing (for production, inject from Python via extra_data if possible)
|
||||
// Example (you should auto-inject this from .py as extra_data or as a hidden field!)
|
||||
promptMap = {
|
||||
"Cheering": "A girl cheering, hands up, energetic, smiling",
|
||||
"Sleeping": "A girl sleeping on her school desk, peaceful, drooling"
|
||||
// ... and so on, matching your action.json
|
||||
};
|
||||
|
||||
dropdown.inputEl.addEventListener("change", () => {
|
||||
const selected = dropdown.inputEl.value;
|
||||
const prompt = promptMap[selected] || "";
|
||||
textarea.value = prompt;
|
||||
widget.value = prompt;
|
||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
// enhanced_prompt.js
|
||||
// For ComfyUI_AnimeCharacterSelect — live insert from dropdown to prompt field
|
||||
|
||||
(function () {
|
||||
// Wait until ComfyUI is fully loaded
|
||||
function setup() {
|
||||
// Scan for our node panels every second (ComfyUI reactivity is slow)
|
||||
setInterval(() => {
|
||||
document.querySelectorAll(".node").forEach(node => {
|
||||
// Only patch nodes with our label!
|
||||
const header = node.querySelector('.node_title');
|
||||
if (!header || !header.innerText.includes("Character + Action Prompt")) return;
|
||||
|
||||
// Get our widgets inside the node
|
||||
const selects = node.querySelectorAll("select");
|
||||
const textarea = node.querySelector("textarea.comfy-multiline-input");
|
||||
if (!textarea) return;
|
||||
|
||||
// Attach only once
|
||||
if (node.dataset.enhancedPromptPatched) return;
|
||||
node.dataset.enhancedPromptPatched = "true";
|
||||
|
||||
selects.forEach(select => {
|
||||
select.addEventListener("change", function () {
|
||||
const value = this.value;
|
||||
if (!value || value.startsWith("Select")) return;
|
||||
|
||||
// Only append if not already present (prevent repeats)
|
||||
let text = textarea.value.trim();
|
||||
// Only add if not already at the start
|
||||
if (!text.includes(value)) {
|
||||
textarea.value = text.length > 0 ? text + ", " + value : value;
|
||||
// Fire event so ComfyUI sees the change
|
||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
// Reset dropdown to first item (optional)
|
||||
this.selectedIndex = 0;
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 1000); // ComfyUI sometimes destroys/rebuilds node UIs
|
||||
}
|
||||
|
||||
// Run after page load
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", setup);
|
||||
} else {
|
||||
setup();
|
||||
}
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user