Update 1.5.0
Fixed bug added information to schemas added new schemas
This commit is contained in:
@@ -11,342 +11,360 @@ PROMPTS_DIR = os.path.join(os.path.dirname(__file__), "web", "prompts")
|
||||
|
||||
def load_prompt_files():
|
||||
|
||||
files = []
|
||||
files = []
|
||||
|
||||
for f in os.listdir(PROMPTS_DIR):
|
||||
if f.endswith(".txt"):
|
||||
files.append(os.path.splitext(f)[0])
|
||||
for f in os.listdir(PROMPTS_DIR):
|
||||
if f.endswith(".txt"):
|
||||
files.append(os.path.splitext(f)[0])
|
||||
|
||||
return sorted(files)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def parse_prompt_file(content: str):
|
||||
"""
|
||||
Parses a prompt file and returns (info, prompt)
|
||||
|
||||
Supported formats:
|
||||
"""
|
||||
Parses a prompt file and returns (info, prompt)
|
||||
|
||||
Supported formats:
|
||||
|
||||
Format1:
|
||||
<info>...</info>
|
||||
<prompt>...</prompt>
|
||||
Format1:
|
||||
<info>...</info>
|
||||
<prompt>...</prompt>
|
||||
|
||||
Format2:
|
||||
<info>...</info>
|
||||
raw text
|
||||
Format2:
|
||||
<info>...</info>
|
||||
raw text
|
||||
|
||||
Format3:
|
||||
<prompt>...</prompt>
|
||||
Format3:
|
||||
<prompt>...</prompt>
|
||||
|
||||
Format4:
|
||||
raw text
|
||||
"""
|
||||
Format4:
|
||||
raw text
|
||||
"""
|
||||
|
||||
info = ""
|
||||
prompt = content.strip()
|
||||
info = ""
|
||||
prompt = content.strip()
|
||||
|
||||
# Extract <info> block if present
|
||||
info_match = re.search(r"<info>([\s\S]*?)</info>", content, re.IGNORECASE)
|
||||
if info_match:
|
||||
info = info_match.group(1).strip()
|
||||
# Extract <info> block if present
|
||||
info_match = re.search(r"<info>([\s\S]*?)</info>", content, re.IGNORECASE)
|
||||
if info_match:
|
||||
info = info_match.group(1).strip()
|
||||
|
||||
# Extract <prompt> block if present
|
||||
prompt_match = re.search(r"<prompt>([\s\S]*?)</prompt>", content, re.IGNORECASE)
|
||||
if prompt_match:
|
||||
prompt = prompt_match.group(1).strip()
|
||||
elif info_match:
|
||||
# remove info block if present
|
||||
prompt = content.replace(info_match.group(0), "").strip()
|
||||
else:
|
||||
# fallback: use entire content
|
||||
prompt = content.strip()
|
||||
# Extract <prompt> block if present
|
||||
prompt_match = re.search(r"<prompt>([\s\S]*?)</prompt>", content, re.IGNORECASE)
|
||||
if prompt_match:
|
||||
prompt = prompt_match.group(1).strip()
|
||||
elif info_match:
|
||||
# remove info block if present
|
||||
prompt = content.replace(info_match.group(0), "").strip()
|
||||
else:
|
||||
# fallback: use entire content
|
||||
prompt = content.strip()
|
||||
|
||||
return info, prompt
|
||||
return info, prompt
|
||||
|
||||
|
||||
class DragosPromptLoaderNode:
|
||||
|
||||
CATEGORY = "DragosScene"
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "load_prompt"
|
||||
CATEGORY = "DragosScene"
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "load_prompt"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": (load_prompt_files(),),
|
||||
},
|
||||
"optional": {
|
||||
"info_text": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_text": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": (load_prompt_files(),),
|
||||
},
|
||||
"optional": {
|
||||
"info_text": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_text": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
def load_prompt(self, prompt, info_text="", prompt_text=""):
|
||||
path = os.path.join(PROMPTS_DIR, prompt + ".txt")
|
||||
def load_prompt(self, prompt, info_text="", prompt_text=""):
|
||||
path = os.path.join(PROMPTS_DIR, prompt + ".txt")
|
||||
|
||||
content = ""
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
content = ""
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
info, parsed_prompt = parse_prompt_file(content)
|
||||
info, parsed_prompt = parse_prompt_file(content)
|
||||
|
||||
# prioritize edited textbox
|
||||
final_prompt = prompt_text.strip() if prompt_text.strip() else parsed_prompt
|
||||
# prioritize edited textbox
|
||||
final_prompt = prompt_text.strip() if prompt_text.strip() else parsed_prompt
|
||||
|
||||
return (final_prompt,)
|
||||
return (final_prompt,)
|
||||
|
||||
|
||||
def load_schema_categories():
|
||||
categories = []
|
||||
|
||||
if not os.path.exists(SCHEMA_DIR):
|
||||
return ["character"]
|
||||
categories = []
|
||||
|
||||
if not os.path.exists(SCHEMA_DIR):
|
||||
return ["character"]
|
||||
|
||||
# Walk through the schema directory and all subdirectories
|
||||
for root, dirs, files in os.walk(SCHEMA_DIR):
|
||||
for file in files:
|
||||
if file.endswith(".json"):
|
||||
# Get the full path of the file
|
||||
full_path = os.path.join(root, file)
|
||||
|
||||
# Get the path relative to the SCHEMA_DIR
|
||||
rel_path = os.path.relpath(full_path, SCHEMA_DIR)
|
||||
|
||||
# Remove the .json extension
|
||||
name = os.path.splitext(rel_path)[0]
|
||||
|
||||
# Normalize path separators to forward slashes for web URLs
|
||||
# (Windows uses backslashes, web needs forward slashes)
|
||||
name = name.replace("\\", "/")
|
||||
|
||||
categories.append(name)
|
||||
# Walk through the schema directory and all subdirectories
|
||||
for root, dirs, files in os.walk(SCHEMA_DIR):
|
||||
for file in files:
|
||||
if file.endswith(".json"):
|
||||
# Get the full path of the file
|
||||
full_path = os.path.join(root, file)
|
||||
|
||||
# Get the path relative to the SCHEMA_DIR
|
||||
rel_path = os.path.relpath(full_path, SCHEMA_DIR)
|
||||
|
||||
# Remove the .json extension
|
||||
name = os.path.splitext(rel_path)[0]
|
||||
|
||||
# Normalize path separators to forward slashes for web URLs
|
||||
# (Windows uses backslashes, web needs forward slashes)
|
||||
name = name.replace("\\", "/")
|
||||
|
||||
categories.append(name)
|
||||
|
||||
return sorted(categories)
|
||||
return sorted(categories)
|
||||
|
||||
|
||||
def is_valid_prompt_var(v):
|
||||
|
||||
if not isinstance(v, dict):
|
||||
return False
|
||||
if not isinstance(v, dict):
|
||||
return False
|
||||
|
||||
if "name" not in v or "value" not in v:
|
||||
return False
|
||||
if "name" not in v or "value" not in v:
|
||||
return False
|
||||
|
||||
if v["value"] is None:
|
||||
return False
|
||||
if v["value"] is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
class DragosVariableNode:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"var_name": ("STRING", {"default": "subject"}),
|
||||
"text": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"var_name": ("STRING", {"default": "subject"}),
|
||||
"text": ("STRING", {"multiline": True, "default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (PROMPT_VAR_TYPE,)
|
||||
FUNCTION = "get_text"
|
||||
CATEGORY = "DragosScene"
|
||||
RETURN_TYPES = (PROMPT_VAR_TYPE,)
|
||||
FUNCTION = "get_text"
|
||||
CATEGORY = "DragosScene"
|
||||
|
||||
def get_text(self, var_name, text):
|
||||
def get_text(self, var_name, text):
|
||||
|
||||
if var_name is None or var_name == "":
|
||||
return (None,)
|
||||
if var_name is None or var_name == "":
|
||||
return (None,)
|
||||
|
||||
return ({
|
||||
"name": var_name,
|
||||
"value": text
|
||||
},)
|
||||
return ({
|
||||
"name": var_name,
|
||||
"value": text
|
||||
},)
|
||||
|
||||
|
||||
class DragosObjectNode:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"obj_name": ("STRING", {"default": "object"}),
|
||||
},
|
||||
"optional": {
|
||||
"input_1": (PROMPT_VAR_TYPE, {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"obj_name": ("STRING", {"default": "object"}),
|
||||
},
|
||||
"optional": {
|
||||
"input_1": (PROMPT_VAR_TYPE, {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = (PROMPT_VAR_TYPE,)
|
||||
FUNCTION = "build_object"
|
||||
CATEGORY = "DragosScene"
|
||||
RETURN_TYPES = (PROMPT_VAR_TYPE,)
|
||||
FUNCTION = "build_object"
|
||||
CATEGORY = "DragosScene"
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
return True
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
return True
|
||||
|
||||
def build_object(self, obj_name, **kwargs):
|
||||
def build_object(self, obj_name, **kwargs):
|
||||
|
||||
if obj_name is None or obj_name == "":
|
||||
return (None,)
|
||||
if obj_name is None or obj_name == "":
|
||||
return (None,)
|
||||
|
||||
combined = {}
|
||||
combined = {}
|
||||
|
||||
for v in kwargs.values():
|
||||
for v in kwargs.values():
|
||||
|
||||
if not is_valid_prompt_var(v):
|
||||
continue
|
||||
if not is_valid_prompt_var(v):
|
||||
continue
|
||||
|
||||
combined[v["name"]] = v["value"]
|
||||
combined[v["name"]] = v["value"]
|
||||
|
||||
if not combined:
|
||||
return (None,)
|
||||
if not combined:
|
||||
return (None,)
|
||||
|
||||
return ({
|
||||
"name": obj_name,
|
||||
"value": combined
|
||||
},)
|
||||
return ({
|
||||
"name": obj_name,
|
||||
"value": combined
|
||||
},)
|
||||
|
||||
|
||||
def deep_merge(base_dict, merge_dict):
|
||||
"""
|
||||
Deep merge merge_dict into base_dict.
|
||||
- If a key exists in both and both values are dicts, merge recursively
|
||||
- Otherwise, merge_dict's value overwrites base_dict's value
|
||||
"""
|
||||
result = dict(base_dict) # shallow copy
|
||||
|
||||
for key, value in merge_dict.items():
|
||||
if (
|
||||
key in result
|
||||
and isinstance(result[key], dict)
|
||||
and isinstance(value, dict)
|
||||
):
|
||||
result[key] = deep_merge(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
"""
|
||||
Deep merge merge_dict into base_dict.
|
||||
- If a key exists in both and both values are dicts, merge recursively
|
||||
- Otherwise, merge_dict's value overwrites base_dict's value
|
||||
"""
|
||||
result = dict(base_dict) # shallow copy
|
||||
|
||||
for key, value in merge_dict.items():
|
||||
if (
|
||||
key in result
|
||||
and isinstance(result[key], dict)
|
||||
and isinstance(value, dict)
|
||||
):
|
||||
result[key] = deep_merge(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class DragosSceneCompiler:
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"input_1": ("PROMPT_VAR", {"forceInput": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"output_json_string": ("STRING", {"multiline": True}),
|
||||
}
|
||||
}
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"input_1": ("PROMPT_VAR", {"forceInput": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"output_json_string": ("STRING", {"multiline": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "compile_json"
|
||||
CATEGORY = "DragosScene"
|
||||
RETURN_TYPES = ("STRING",)
|
||||
OUTPUT_NODE = True
|
||||
FUNCTION = "compile_json"
|
||||
CATEGORY = "DragosScene"
|
||||
|
||||
@staticmethod
|
||||
def _unwrap_prompt_var(v):
|
||||
while isinstance(v, tuple) and len(v) == 1:
|
||||
v = v[0]
|
||||
return v
|
||||
@staticmethod
|
||||
def _unwrap_prompt_var(v):
|
||||
while isinstance(v, tuple) and len(v) == 1:
|
||||
v = v[0]
|
||||
return v
|
||||
|
||||
@staticmethod
|
||||
def _compact_for_llm(text: str) -> str:
|
||||
"""
|
||||
Replace tabs and newlines with spaces and collapse multiple spaces.
|
||||
This reduces token count and improves semantic locality.
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
@staticmethod
|
||||
def _compact_for_llm(text: str) -> str:
|
||||
"""
|
||||
Replace tabs and newlines with spaces and collapse multiple spaces.
|
||||
This reduces token count and improves semantic locality.
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# Replace newline and tab with space
|
||||
text = re.sub(r"[\n\t]+", " ", text)
|
||||
# Replace newline and tab with space
|
||||
text = re.sub(r"[\n\t]+", " ", text)
|
||||
|
||||
# Collapse multiple spaces into one
|
||||
text = re.sub(r" {2,}", " ", text)
|
||||
# Collapse multiple spaces into one
|
||||
text = re.sub(r" {2,}", " ", text)
|
||||
|
||||
return text.strip()
|
||||
return text.strip()
|
||||
|
||||
def compile_json(self, **kwargs):
|
||||
def compile_json(self, **kwargs):
|
||||
|
||||
hidden_json = kwargs.get("output_json_string")
|
||||
hidden_json = kwargs.get("output_json_string")
|
||||
|
||||
if hidden_json:
|
||||
compact = self._compact_for_llm(hidden_json)
|
||||
return (compact,)
|
||||
if hidden_json:
|
||||
compact = self._compact_for_llm(hidden_json)
|
||||
return (compact,)
|
||||
|
||||
scene = {}
|
||||
scene = {}
|
||||
|
||||
for key, v in kwargs.items():
|
||||
unwrapped = self._unwrap_prompt_var(v)
|
||||
if isinstance(unwrapped, dict) and "name" in unwrapped and "value" in unwrapped:
|
||||
entry_name = unwrapped["name"]
|
||||
entry_value = unwrapped["value"]
|
||||
|
||||
# Check if key already exists and merge if both are dicts
|
||||
if entry_name in scene and isinstance(scene[entry_name], dict) and isinstance(entry_value, dict):
|
||||
scene[entry_name] = deep_merge(scene[entry_name], entry_value)
|
||||
else:
|
||||
scene[entry_name] = entry_value
|
||||
for key, v in kwargs.items():
|
||||
unwrapped = self._unwrap_prompt_var(v)
|
||||
if isinstance(unwrapped, dict) and "name" in unwrapped and "value" in unwrapped:
|
||||
entry_name = unwrapped["name"]
|
||||
entry_value = unwrapped["value"]
|
||||
|
||||
# Check if key already exists and merge if both are dicts
|
||||
if entry_name in scene and isinstance(scene[entry_name], dict) and isinstance(entry_value, dict):
|
||||
scene[entry_name] = deep_merge(scene[entry_name], entry_value)
|
||||
else:
|
||||
scene[entry_name] = entry_value
|
||||
|
||||
json_out = json.dumps(scene, indent="\t", ensure_ascii=False)
|
||||
json_out = json.dumps(scene, indent="\t", ensure_ascii=False)
|
||||
|
||||
compact = self._compact_for_llm(json_out)
|
||||
compact = self._compact_for_llm(json_out)
|
||||
|
||||
return (compact,)
|
||||
return (compact,)
|
||||
|
||||
class DragosStructuredBuilderNode:
|
||||
|
||||
CATEGORY = "DragosScene"
|
||||
RETURN_TYPES = (PROMPT_VAR_TYPE,)
|
||||
FUNCTION = "build"
|
||||
CATEGORY = "DragosScene"
|
||||
RETURN_TYPES = (PROMPT_VAR_TYPE,)
|
||||
FUNCTION = "build"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"category": (load_schema_categories(),),
|
||||
},
|
||||
"optional": {
|
||||
"input_1": (PROMPT_VAR_TYPE, {"forceInput": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"json_data": ("STRING",),
|
||||
}
|
||||
}
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"category": (load_schema_categories(),),
|
||||
},
|
||||
"optional": {
|
||||
"input_1": (PROMPT_VAR_TYPE, {"forceInput": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"json_data": ("STRING",),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
return True
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(cls, **kwargs):
|
||||
return True
|
||||
|
||||
def build(self, category, json_data, **kwargs):
|
||||
|
||||
try:
|
||||
parsed = json.loads(json_data) if json_data else {}
|
||||
except Exception:
|
||||
parsed = {}
|
||||
|
||||
meta = parsed.get("_meta", {})
|
||||
base_data = parsed.get("data", {})
|
||||
|
||||
# Collect all input values from kwargs (dynamic inputs)
|
||||
input_values = {}
|
||||
for v in kwargs.values():
|
||||
if not is_valid_prompt_var(v):
|
||||
continue
|
||||
input_values[v["name"]] = v["value"]
|
||||
|
||||
# Merge input values with base data
|
||||
# Input values override base data
|
||||
merged_data = deep_merge(base_data, input_values)
|
||||
|
||||
return ({
|
||||
"name": meta.get("category", category),
|
||||
"value": merged_data,
|
||||
"_meta": meta
|
||||
},)
|
||||
def build(self, category, json_data="", **kwargs):
|
||||
|
||||
try:
|
||||
parsed = json.loads(json_data) if json_data else {}
|
||||
except Exception:
|
||||
parsed = {}
|
||||
|
||||
meta = parsed.get("_meta", {})
|
||||
base_data = parsed.get("data", {})
|
||||
|
||||
# Get this node's category (e.g., "character") to validate extensions
|
||||
my_category = meta.get("category", "")
|
||||
|
||||
# Collect all input values from kwargs (dynamic inputs)
|
||||
input_values = {}
|
||||
extensions_to_merge = []
|
||||
|
||||
for v in kwargs.values():
|
||||
if not is_valid_prompt_var(v):
|
||||
continue
|
||||
|
||||
# Check if this input is an extension schema
|
||||
v_meta = v.get("_meta", {})
|
||||
if v_meta and v_meta.get("extension"):
|
||||
# ONLY merge if the extension's target matches this node's category!
|
||||
if v_meta.get("extension") == my_category:
|
||||
extensions_to_merge.append(v["value"])
|
||||
else:
|
||||
# Normal variable/object input
|
||||
input_values[v["name"]] = v["value"]
|
||||
|
||||
# Merge standard input values with base data
|
||||
merged_data = deep_merge(base_data, input_values)
|
||||
|
||||
# Deep merge extensions directly into the root (e.g., merging "top": "shirt" into character)
|
||||
for ext_data in extensions_to_merge:
|
||||
if isinstance(ext_data, dict):
|
||||
merged_data = deep_merge(merged_data, ext_data)
|
||||
|
||||
return ({
|
||||
"name": meta.get("category", category),
|
||||
"value": merged_data,
|
||||
"_meta": meta
|
||||
},)
|
||||
+602
-562
File diff suppressed because it is too large
Load Diff
@@ -1,707 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { ComfyWidgets } from "../../scripts/widgets.js";
|
||||
|
||||
|
||||
function isNodeActive(node)
|
||||
{
|
||||
if (!node) return false;
|
||||
|
||||
if (node.mode === 2) return false;
|
||||
if (node.mode === 4) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getWidgetValue(node, names)
|
||||
{
|
||||
if (!node.widgets) return null;
|
||||
|
||||
for (const name of names)
|
||||
{
|
||||
const w = node.widgets.find(w => w.name === name);
|
||||
if (w) return w.value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function deepMergeObjects(base, override)
|
||||
{
|
||||
const result = { ...base };
|
||||
|
||||
for (const key in override)
|
||||
{
|
||||
if (
|
||||
key in result &&
|
||||
typeof result[key] === "object" &&
|
||||
result[key] !== null &&
|
||||
!Array.isArray(result[key]) &&
|
||||
typeof override[key] === "object" &&
|
||||
override[key] !== null &&
|
||||
!Array.isArray(override[key])
|
||||
)
|
||||
{
|
||||
result[key] = deepMergeObjects(result[key], override[key]);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[key] = override[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getValueRecursive(node, graph)
|
||||
{
|
||||
if (!node) return undefined;
|
||||
if (!isNodeActive(node)) return undefined;
|
||||
|
||||
if (node.comfyClass === "DragosVariable")
|
||||
return getWidgetValue(node, ["text"]) ?? "";
|
||||
|
||||
if (node.comfyClass === "DragosStructuredBuilder")
|
||||
{
|
||||
// First, build the base result from widgets (schema data)
|
||||
const result = {};
|
||||
|
||||
for (const widget of node.widgets || [])
|
||||
{
|
||||
if (!widget.dragosPath) continue;
|
||||
if (widget.hidden) continue;
|
||||
if (widget.value === "" || widget.value == null) continue;
|
||||
|
||||
const parts = widget.dragosPath.split(".");
|
||||
let current = result;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++)
|
||||
{
|
||||
const part = parts[i];
|
||||
|
||||
if (!(part in current))
|
||||
current[part] = {};
|
||||
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
current[parts[parts.length - 1]] = widget.value;
|
||||
}
|
||||
|
||||
// Now merge any connected inputs (DragosVariable or DragosObject)
|
||||
if (node.inputs)
|
||||
{
|
||||
for (const input of node.inputs)
|
||||
{
|
||||
if (!input || input.link == null) continue;
|
||||
if (!input.name || !input.name.startsWith("input_")) continue;
|
||||
|
||||
const link = graph.links[input.link];
|
||||
if (!link) continue;
|
||||
|
||||
const origin = graph.getNodeById(link.origin_id);
|
||||
if (!origin || !isNodeActive(origin)) continue;
|
||||
|
||||
// Get the name from the connected node
|
||||
const inputName = getWidgetValue(origin, ["var_name", "obj_name"]);
|
||||
const inputValue = getValueRecursive(origin, graph);
|
||||
|
||||
if (inputName && inputValue !== undefined)
|
||||
{
|
||||
// If it's a simple value, add/override directly
|
||||
// If it's an object, merge it
|
||||
if (typeof inputValue === "object" && inputValue !== null)
|
||||
{
|
||||
result[inputName] = deepMergeObjects(result[inputName] || {}, inputValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[inputName] = inputValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (node.comfyClass === "DragosObject")
|
||||
{
|
||||
const result = {};
|
||||
|
||||
for (const input of node.inputs || [])
|
||||
{
|
||||
if (input.link == null) continue;
|
||||
|
||||
const link = graph.links[input.link];
|
||||
if (!link) continue;
|
||||
|
||||
const origin = graph.getNodeById(link.origin_id);
|
||||
if (!isNodeActive(origin)) continue;
|
||||
|
||||
const key =
|
||||
getWidgetValue(origin, ["var_name", "obj_name"])
|
||||
?? input.name;
|
||||
|
||||
const value = getValueRecursive(origin, graph);
|
||||
|
||||
if (value !== undefined)
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
function refreshNode(node, graph, previewWidget, syncPreviewHeight)
|
||||
{
|
||||
if (!node || !graph || !node.inputs)
|
||||
return;
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 1 — Collect ordered origin nodes (stable order)
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
const orderedOrigins = [];
|
||||
|
||||
for (let i = 0; i < node.inputs.length; i++)
|
||||
{
|
||||
const input = node.inputs[i];
|
||||
|
||||
if (!input || input.link == null)
|
||||
continue;
|
||||
|
||||
const link = graph.links[input.link];
|
||||
if (!link)
|
||||
continue;
|
||||
|
||||
const origin = graph.getNodeById(link.origin_id);
|
||||
|
||||
if (!origin)
|
||||
continue;
|
||||
|
||||
if (!isNodeActive(origin))
|
||||
continue;
|
||||
|
||||
orderedOrigins.push(origin);
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 2 — Build intermediate structure
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
const built = [];
|
||||
|
||||
for (let i = 0; i < orderedOrigins.length; i++)
|
||||
{
|
||||
const origin = orderedOrigins[i];
|
||||
|
||||
const value =
|
||||
getValueRecursive(origin, graph);
|
||||
|
||||
if (
|
||||
value == null ||
|
||||
(typeof value === "object" &&
|
||||
Object.keys(value).length === 0)
|
||||
)
|
||||
continue;
|
||||
|
||||
|
||||
let category = "";
|
||||
let extension = "";
|
||||
|
||||
|
||||
// Read _meta from json_data widget ONCE
|
||||
const jsonWidget =
|
||||
origin.widgets?.find(
|
||||
w => w.name === "json_data"
|
||||
);
|
||||
|
||||
if (jsonWidget?.value)
|
||||
{
|
||||
try
|
||||
{
|
||||
const parsed =
|
||||
JSON.parse(jsonWidget.value);
|
||||
|
||||
const meta = parsed?._meta;
|
||||
|
||||
if (meta)
|
||||
{
|
||||
category =
|
||||
String(
|
||||
meta.display_name ??
|
||||
meta.displayName ??
|
||||
meta.category ??
|
||||
""
|
||||
).toLowerCase();
|
||||
|
||||
extension =
|
||||
String(
|
||||
meta.extension ?? ""
|
||||
).toLowerCase();
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
|
||||
|
||||
// Fallback category detection
|
||||
if (!category)
|
||||
{
|
||||
category =
|
||||
String(
|
||||
getWidgetValue(
|
||||
origin,
|
||||
["category", "obj_name", "var_name"]
|
||||
) || ""
|
||||
).toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
built.push({
|
||||
category,
|
||||
extension,
|
||||
value,
|
||||
sceneKey: null
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 3 — Merge into final scene
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
const scene = {};
|
||||
|
||||
let characterCount = 0;
|
||||
|
||||
|
||||
for (let i = 0; i < built.length; i++)
|
||||
{
|
||||
const entry = built[i];
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// TOP-LEVEL
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
if (!entry.extension)
|
||||
{
|
||||
if (entry.category === "character")
|
||||
{
|
||||
characterCount++;
|
||||
|
||||
const key =
|
||||
`character_${characterCount}`;
|
||||
|
||||
// Check if key already exists and merge if both are objects
|
||||
if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object")
|
||||
{
|
||||
scene[key] = deepMergeObjects(scene[key], entry.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
scene[key] = entry.value;
|
||||
}
|
||||
|
||||
entry.sceneKey = key;
|
||||
}
|
||||
else if (entry.category)
|
||||
{
|
||||
const key = entry.category;
|
||||
|
||||
// Check if key already exists and merge if both are objects
|
||||
if (scene[key] && typeof scene[key] === "object" && typeof entry.value === "object")
|
||||
{
|
||||
scene[key] = deepMergeObjects(scene[key], entry.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
scene[key] = entry.value;
|
||||
}
|
||||
|
||||
entry.sceneKey = key;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// EXTENSION MERGE
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
const targetCategory =
|
||||
entry.extension;
|
||||
|
||||
for (let j = i - 1; j >= 0; j--)
|
||||
{
|
||||
const candidate = built[j];
|
||||
|
||||
if (
|
||||
candidate.category !== targetCategory
|
||||
)
|
||||
continue;
|
||||
|
||||
const targetKey =
|
||||
candidate.sceneKey;
|
||||
|
||||
if (!targetKey)
|
||||
break;
|
||||
|
||||
if (!scene[targetKey])
|
||||
scene[targetKey] = {};
|
||||
|
||||
// Use deep merge instead of Object.assign for nested objects
|
||||
scene[targetKey] = deepMergeObjects(scene[targetKey], entry.value);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 4 — Remove empty objects (deep clean)
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
function removeEmpty(obj)
|
||||
{
|
||||
if (!obj || typeof obj !== "object")
|
||||
return obj;
|
||||
|
||||
for (const key in obj)
|
||||
{
|
||||
const val =
|
||||
removeEmpty(obj[key]);
|
||||
|
||||
if (
|
||||
val == null ||
|
||||
(typeof val === "object" &&
|
||||
Object.keys(val).length === 0)
|
||||
)
|
||||
{
|
||||
delete obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
removeEmpty(scene);
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 5 — Convert to JSON
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
const json =
|
||||
JSON.stringify(scene, null, "\t");
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 6 — Update preview widget (only if changed)
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
if (
|
||||
previewWidget &&
|
||||
previewWidget.value !== json
|
||||
)
|
||||
{
|
||||
previewWidget.value = json;
|
||||
|
||||
if (syncPreviewHeight)
|
||||
syncPreviewHeight();
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 7 — Hidden output widget
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
let hiddenOutput =
|
||||
node.widgets.find(
|
||||
w => w.name === "output_json_string"
|
||||
);
|
||||
|
||||
if (!hiddenOutput)
|
||||
{
|
||||
hiddenOutput =
|
||||
node.addWidget(
|
||||
"text",
|
||||
"output_json_string",
|
||||
"",
|
||||
() => {}
|
||||
);
|
||||
|
||||
hiddenOutput.hidden = true;
|
||||
hiddenOutput.computeSize =
|
||||
() => [0, -8];
|
||||
hiddenOutput.draw =
|
||||
() => {};
|
||||
}
|
||||
|
||||
hiddenOutput.value = json;
|
||||
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// STEP 8 — Dynamic inputs (CORRECT INDEX-SAFE VERSION)
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
const dynamicInputs = node.inputs
|
||||
.map((slot, realIndex) => ({
|
||||
slot,
|
||||
realIndex,
|
||||
index: (
|
||||
slot &&
|
||||
slot.name &&
|
||||
slot.name.startsWith("input_")
|
||||
)
|
||||
? parseInt(slot.name.substring(6)) || 0
|
||||
: -1
|
||||
}))
|
||||
.filter(x => x.index >= 0)
|
||||
.sort((a, b) => a.index - b.index);
|
||||
|
||||
|
||||
if (dynamicInputs.length === 0)
|
||||
return;
|
||||
|
||||
|
||||
let changed = false;
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Add new slot if last is connected
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
const last = dynamicInputs[dynamicInputs.length - 1];
|
||||
|
||||
if (last.slot.link != null)
|
||||
{
|
||||
node.addInput(
|
||||
`input_${last.index + 1}`,
|
||||
"PROMPT_VAR"
|
||||
);
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Remove trailing empty slots SAFELY using REAL INDEX
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
while (dynamicInputs.length > 1)
|
||||
{
|
||||
const lastEntry =
|
||||
dynamicInputs[dynamicInputs.length - 1];
|
||||
|
||||
const prevEntry =
|
||||
dynamicInputs[dynamicInputs.length - 2];
|
||||
|
||||
if (
|
||||
lastEntry.slot.link == null &&
|
||||
prevEntry.slot.link == null
|
||||
)
|
||||
{
|
||||
// find CURRENT real index (important)
|
||||
const currentRealIndex =
|
||||
node.inputs.findIndex(
|
||||
s => s === lastEntry.slot
|
||||
);
|
||||
|
||||
if (currentRealIndex !== -1)
|
||||
{
|
||||
node.removeInput(currentRealIndex);
|
||||
|
||||
dynamicInputs.pop();
|
||||
|
||||
changed = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (changed)
|
||||
{
|
||||
node.setDirtyCanvas(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
app.registerExtension({
|
||||
|
||||
name: "Dragos.SceneBuilder.Dynamic",
|
||||
|
||||
async nodeCreated(node)
|
||||
{
|
||||
if (
|
||||
node.comfyClass !== "DragosObject"
|
||||
&&
|
||||
node.comfyClass !== "DragosSceneCompiler"
|
||||
&&
|
||||
node.comfyClass !== "DragosStructuredBuilder"
|
||||
) return;
|
||||
|
||||
let previewWidget = null;
|
||||
let syncPreviewHeight = null;
|
||||
|
||||
if (node.comfyClass === "DragosSceneCompiler")
|
||||
{
|
||||
previewWidget =
|
||||
ComfyWidgets.STRING(
|
||||
node,
|
||||
"preview",
|
||||
["STRING", { multiline: true }],
|
||||
app
|
||||
).widget;
|
||||
|
||||
previewWidget.inputEl.readOnly = true;
|
||||
previewWidget.inputEl.style.fontSize = "11px";
|
||||
previewWidget.inputEl.style.resize = "none";
|
||||
|
||||
|
||||
syncPreviewHeight = function()
|
||||
{
|
||||
if (!previewWidget?.inputEl) return;
|
||||
if (!node.size) return;
|
||||
|
||||
const HEADER = LiteGraph.NODE_TITLE_HEIGHT || 30;
|
||||
const SLOT = LiteGraph.NODE_SLOT_HEIGHT || 20;
|
||||
const WIDGET_SPACING = 4;
|
||||
const BOTTOM_PADDING = 15; // Creates the consistent buffer at the bottom
|
||||
|
||||
let widgetY = HEADER;
|
||||
|
||||
// Calculate the Y position where the preview widget starts
|
||||
for (const w of node.widgets)
|
||||
{
|
||||
if (w === previewWidget) break;
|
||||
const size = w.computeSize?.(node.size[0]);
|
||||
widgetY += (size?.[1] || 20) + WIDGET_SPACING;
|
||||
}
|
||||
|
||||
// Calculate height occupied by inputs on the left
|
||||
// We subtract this to create the "buffer" so the text box doesn't overlap the inputs area
|
||||
const inputCount = node.inputs?.length || 0;
|
||||
const inputsHeight = inputCount * SLOT - SLOT;
|
||||
|
||||
// Calculate available height
|
||||
// Node Height - (Widgets Above) - (Inputs Height) - (Bottom Padding)
|
||||
const available = node.size[1] - widgetY - inputsHeight - BOTTOM_PADDING;
|
||||
|
||||
// Set height.
|
||||
// We use Math.max(20) instead of 50.
|
||||
// This allows the box to be small enough to FIT in small nodes (fixing Image_1)
|
||||
// while the calculation above ensures it stops before the inputs (fixing Image_3).
|
||||
previewWidget.inputEl.style.height = Math.max(available, 20) + "px";
|
||||
};
|
||||
|
||||
|
||||
const origOnResize = node.onResize;
|
||||
|
||||
node.onResize = function()
|
||||
{
|
||||
if (origOnResize)
|
||||
origOnResize.apply(this, arguments);
|
||||
|
||||
syncPreviewHeight();
|
||||
};
|
||||
|
||||
|
||||
// force correct node size first
|
||||
requestAnimationFrame(() =>
|
||||
{
|
||||
if (node.computeSize)
|
||||
{
|
||||
const newSize = node.computeSize();
|
||||
node.setSize(newSize);
|
||||
}
|
||||
|
||||
syncPreviewHeight();
|
||||
|
||||
node.setDirtyCanvas(true, true);
|
||||
});
|
||||
|
||||
setTimeout(() =>
|
||||
{
|
||||
if (node.computeSize)
|
||||
{
|
||||
const newSize = node.computeSize();
|
||||
node.setSize(newSize);
|
||||
}
|
||||
|
||||
syncPreviewHeight();
|
||||
|
||||
node.setDirtyCanvas(true, true);
|
||||
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
const graph = app.graph;
|
||||
|
||||
const update = () =>
|
||||
refreshNode(
|
||||
node,
|
||||
graph,
|
||||
previewWidget,
|
||||
syncPreviewHeight
|
||||
);
|
||||
|
||||
|
||||
const origConnectionsChange = node.onConnectionsChange;
|
||||
|
||||
node.onConnectionsChange = function ()
|
||||
{
|
||||
if (origConnectionsChange)
|
||||
origConnectionsChange.apply(this, arguments);
|
||||
|
||||
update();
|
||||
};
|
||||
|
||||
|
||||
const origWidgetChanged = node.onWidgetChanged;
|
||||
|
||||
node.onWidgetChanged = function ()
|
||||
{
|
||||
if (origWidgetChanged)
|
||||
origWidgetChanged.apply(this, arguments);
|
||||
|
||||
update();
|
||||
};
|
||||
|
||||
|
||||
const timer = setInterval(() =>
|
||||
{
|
||||
if (!graph._nodes.includes(node))
|
||||
{
|
||||
clearInterval(timer);
|
||||
return;
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
}, 500);
|
||||
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
});
|
||||
+492
-445
File diff suppressed because it is too large
Load Diff
@@ -8,46 +8,6 @@
|
||||
"from": "Final Fantasy IX (FF9)"
|
||||
},
|
||||
"age": "6",
|
||||
"placement": {
|
||||
"frame_position": [
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-right"
|
||||
],
|
||||
"proximity_to_camera": [
|
||||
"",
|
||||
"foreground",
|
||||
"midground",
|
||||
"background",
|
||||
"close",
|
||||
"medium",
|
||||
"far"
|
||||
],
|
||||
"orientation": [
|
||||
"",
|
||||
"facing camera",
|
||||
"facing left",
|
||||
"facing right",
|
||||
"facing away",
|
||||
"profile left",
|
||||
"profile right",
|
||||
"three-quarter left",
|
||||
"three-quarter right"
|
||||
],
|
||||
"looking_at": [
|
||||
"",
|
||||
"at camera",
|
||||
"away from camera",
|
||||
"other"
|
||||
],
|
||||
"relative_to": ""
|
||||
},
|
||||
"appearance": {
|
||||
"physique": {
|
||||
"build": "petite, child",
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"_meta": {
|
||||
"display_name": "Character"
|
||||
},
|
||||
"_static": {
|
||||
"gender": "Female",
|
||||
"character_type": "Animal"
|
||||
},
|
||||
"name": "",
|
||||
"age": "",
|
||||
"from": "",
|
||||
"species": [
|
||||
"",
|
||||
"Wolf",
|
||||
"Fox",
|
||||
"Feline",
|
||||
"Canine",
|
||||
"Equine",
|
||||
"Bovine",
|
||||
"Ursine",
|
||||
"Reptile",
|
||||
"Avian",
|
||||
"Dragon",
|
||||
"Lamia",
|
||||
"Naga",
|
||||
"Griffin",
|
||||
"Unicorn",
|
||||
"Wyvern",
|
||||
"Kirin",
|
||||
"other"
|
||||
],
|
||||
"appearance": {
|
||||
"body_build": [
|
||||
"",
|
||||
"lean",
|
||||
"athletic",
|
||||
"toned",
|
||||
"stocky",
|
||||
"heavy",
|
||||
"slender",
|
||||
"lithe",
|
||||
"other"
|
||||
],
|
||||
"coat_type": [
|
||||
"",
|
||||
"fur",
|
||||
"scales",
|
||||
"feathers",
|
||||
"skin",
|
||||
"chitin",
|
||||
"other"
|
||||
],
|
||||
"color_primary": [
|
||||
"",
|
||||
"black",
|
||||
"white",
|
||||
"gray",
|
||||
"brown",
|
||||
"tan",
|
||||
"red",
|
||||
"golden",
|
||||
"green",
|
||||
"blue",
|
||||
"other"
|
||||
],
|
||||
"color_secondary": [
|
||||
"",
|
||||
"black",
|
||||
"white",
|
||||
"gray",
|
||||
"brown",
|
||||
"tan",
|
||||
"red",
|
||||
"golden",
|
||||
"green",
|
||||
"blue",
|
||||
"none",
|
||||
"other"
|
||||
],
|
||||
"pattern": [
|
||||
"",
|
||||
"solid",
|
||||
"striped",
|
||||
"spotted",
|
||||
"rosettes",
|
||||
"bi-color",
|
||||
"tri-color",
|
||||
"brindle",
|
||||
"other"
|
||||
],
|
||||
"head_features": {
|
||||
"ears": [
|
||||
"",
|
||||
"pointed",
|
||||
"rounded",
|
||||
"floppy",
|
||||
"horned",
|
||||
"finned",
|
||||
"none",
|
||||
"other"
|
||||
],
|
||||
"horns_antlers": [
|
||||
"",
|
||||
"none",
|
||||
"antlers",
|
||||
"ram horns",
|
||||
"straight horns",
|
||||
"curved horns",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"special_traits": [
|
||||
"",
|
||||
"wings",
|
||||
"multiple tails",
|
||||
"mane",
|
||||
"spikes",
|
||||
"frills",
|
||||
"bioluminescence",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"anatomy": {
|
||||
"genitals": {
|
||||
"type": [
|
||||
"",
|
||||
"standard",
|
||||
"cloaca",
|
||||
"other"
|
||||
],
|
||||
"size": [
|
||||
"",
|
||||
"small",
|
||||
"average",
|
||||
"large",
|
||||
"other"
|
||||
],
|
||||
"shape": [
|
||||
"",
|
||||
"slit",
|
||||
"rounded",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"secondary_sex_characteristics": {
|
||||
"teats_presence": ["", "yes", "no"],
|
||||
"teats_count": ["", "2", "4", "6", "8", "other"],
|
||||
"plumpness": [
|
||||
"",
|
||||
"flat",
|
||||
"moderate",
|
||||
"full",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"physique_details": {
|
||||
"paws_claws": ["", "paws", "claws", "hooves", "talons", "other"],
|
||||
"tail": ["", "long", "short", "fluffy", "hairless", "none", "other"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,46 +29,6 @@
|
||||
],
|
||||
"hybrid_note": ""
|
||||
},
|
||||
"placement": {
|
||||
"frame_position": [
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-right"
|
||||
],
|
||||
"proximity_to_camera": [
|
||||
"",
|
||||
"foreground",
|
||||
"midground",
|
||||
"background",
|
||||
"close",
|
||||
"medium",
|
||||
"far"
|
||||
],
|
||||
"orientation": [
|
||||
"",
|
||||
"facing camera",
|
||||
"facing left",
|
||||
"facing right",
|
||||
"facing away",
|
||||
"profile left",
|
||||
"profile right",
|
||||
"three-quarter left",
|
||||
"three-quarter right"
|
||||
],
|
||||
"looking_at": [
|
||||
"",
|
||||
"at camera",
|
||||
"away from camera",
|
||||
"other"
|
||||
],
|
||||
"relative_to": ""
|
||||
},
|
||||
"appearance": {
|
||||
"body_form": [
|
||||
"",
|
||||
|
||||
@@ -8,46 +8,6 @@
|
||||
"name":"",
|
||||
"age":"",
|
||||
"from":"",
|
||||
"placement": {
|
||||
"frame_position": [
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-right"
|
||||
],
|
||||
"proximity_to_camera": [
|
||||
"",
|
||||
"foreground",
|
||||
"midground",
|
||||
"background",
|
||||
"close",
|
||||
"medium",
|
||||
"far"
|
||||
],
|
||||
"orientation": [
|
||||
"",
|
||||
"facing camera",
|
||||
"facing left",
|
||||
"facing right",
|
||||
"facing away",
|
||||
"profile left",
|
||||
"profile right",
|
||||
"three-quarter left",
|
||||
"three-quarter right"
|
||||
],
|
||||
"looking_at": [
|
||||
"",
|
||||
"at camera",
|
||||
"away from camera",
|
||||
"other"
|
||||
],
|
||||
"relative_to": ""
|
||||
},
|
||||
"appearance": {
|
||||
"body_form": [
|
||||
"",
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"_meta": {
|
||||
"display_name": "Character"
|
||||
},
|
||||
"_static": {
|
||||
"gender": "Male",
|
||||
"character_type": "Animal"
|
||||
},
|
||||
"name": "",
|
||||
"age": "",
|
||||
"from": "",
|
||||
"species": [
|
||||
"",
|
||||
"Wolf",
|
||||
"Fox",
|
||||
"Feline",
|
||||
"Canine",
|
||||
"Equine",
|
||||
"Bovine",
|
||||
"Ursine",
|
||||
"Reptile",
|
||||
"Avian",
|
||||
"Dragon",
|
||||
"Lamia",
|
||||
"Naga",
|
||||
"Griffin",
|
||||
"Unicorn",
|
||||
"Wyvern",
|
||||
"Kirin",
|
||||
"other"
|
||||
],
|
||||
"appearance": {
|
||||
"body_build": [
|
||||
"",
|
||||
"lean",
|
||||
"athletic",
|
||||
"muscular",
|
||||
"stocky",
|
||||
"heavy",
|
||||
"slender",
|
||||
"lanky",
|
||||
"other"
|
||||
],
|
||||
"coat_type": [
|
||||
"",
|
||||
"fur",
|
||||
"scales",
|
||||
"feathers",
|
||||
"skin",
|
||||
"chitin",
|
||||
"other"
|
||||
],
|
||||
"color_primary": [
|
||||
"",
|
||||
"black",
|
||||
"white",
|
||||
"gray",
|
||||
"brown",
|
||||
"tan",
|
||||
"red",
|
||||
"golden",
|
||||
"green",
|
||||
"blue",
|
||||
"other"
|
||||
],
|
||||
"color_secondary": [
|
||||
"",
|
||||
"black",
|
||||
"white",
|
||||
"gray",
|
||||
"brown",
|
||||
"tan",
|
||||
"red",
|
||||
"golden",
|
||||
"green",
|
||||
"blue",
|
||||
"none",
|
||||
"other"
|
||||
],
|
||||
"pattern": [
|
||||
"",
|
||||
"solid",
|
||||
"striped",
|
||||
"spotted",
|
||||
"rosettes",
|
||||
"bi-color",
|
||||
"tri-color",
|
||||
"brindle",
|
||||
"other"
|
||||
],
|
||||
"head_features": {
|
||||
"ears": [
|
||||
"",
|
||||
"pointed",
|
||||
"rounded",
|
||||
"floppy",
|
||||
"horned",
|
||||
"finned",
|
||||
"none",
|
||||
"other"
|
||||
],
|
||||
"horns_antlers": [
|
||||
"",
|
||||
"none",
|
||||
"antlers",
|
||||
"ram horns",
|
||||
"straight horns",
|
||||
"curved horns",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"special_traits": [
|
||||
"",
|
||||
"wings",
|
||||
"multiple tails",
|
||||
"mane",
|
||||
"spikes",
|
||||
"frills",
|
||||
"bioluminescence",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"anatomy": {
|
||||
"genitals": {
|
||||
"state": ["", "flaccid", "semi-erect", "fully-erect", "extended"],
|
||||
"configuration": [
|
||||
"",
|
||||
"humanoid",
|
||||
"canine (knot)",
|
||||
"equine (flare)",
|
||||
"feline (barbs)",
|
||||
"reptilian (slit/hemi)",
|
||||
"other"
|
||||
],
|
||||
"genitals_size": [
|
||||
"",
|
||||
"small",
|
||||
"average",
|
||||
"large",
|
||||
"huge"
|
||||
],
|
||||
"sheath": {
|
||||
"sheath_presence": ["", "yes", "no"],
|
||||
"sheath_type": ["", "fur", "skin", "scales"]
|
||||
},
|
||||
"knot": {
|
||||
"knot_presence": ["", "yes", "no"],
|
||||
"knot_size": ["", "small", "medium", "large"]
|
||||
},
|
||||
"median_ring": {
|
||||
"median_ring_presence": ["", "yes", "no"],
|
||||
"median_ring_size": ["", "small", "medium", "large"]
|
||||
},
|
||||
"pubic_fur": {
|
||||
"pubic_fur_presence": ["", "yes", "no"],
|
||||
"pubic_fur_density": ["", "sparse", "moderate", "dense"],
|
||||
"pubic_fur_style": ["", "natural", "trimmed", "shaved"]
|
||||
}
|
||||
},
|
||||
"physique_details": {
|
||||
"paws_claws": ["", "paws", "claws", "hooves", "talons", "other"],
|
||||
"tail": ["", "long", "short", "fluffy", "hairless", "none", "other"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,46 +29,6 @@
|
||||
],
|
||||
"hybrid_note": ""
|
||||
},
|
||||
"placement": {
|
||||
"frame_position": [
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-right"
|
||||
],
|
||||
"proximity_to_camera": [
|
||||
"",
|
||||
"foreground",
|
||||
"midground",
|
||||
"background",
|
||||
"close",
|
||||
"medium",
|
||||
"far"
|
||||
],
|
||||
"orientation": [
|
||||
"",
|
||||
"facing camera",
|
||||
"facing left",
|
||||
"facing right",
|
||||
"facing away",
|
||||
"profile left",
|
||||
"profile right",
|
||||
"three-quarter left",
|
||||
"three-quarter right"
|
||||
],
|
||||
"looking_at": [
|
||||
"",
|
||||
"at camera",
|
||||
"away from camera",
|
||||
"other"
|
||||
],
|
||||
"relative_to": ""
|
||||
},
|
||||
"appearance": {
|
||||
"body_build": [
|
||||
"",
|
||||
|
||||
@@ -8,46 +8,6 @@
|
||||
"name":"",
|
||||
"age":"",
|
||||
"from":"",
|
||||
"placement": {
|
||||
"frame_position": [
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-right"
|
||||
],
|
||||
"proximity_to_camera": [
|
||||
"",
|
||||
"foreground",
|
||||
"midground",
|
||||
"background",
|
||||
"close",
|
||||
"medium",
|
||||
"far"
|
||||
],
|
||||
"orientation": [
|
||||
"",
|
||||
"facing camera",
|
||||
"facing left",
|
||||
"facing right",
|
||||
"facing away",
|
||||
"profile left",
|
||||
"profile right",
|
||||
"three-quarter left",
|
||||
"three-quarter right"
|
||||
],
|
||||
"looking_at": [
|
||||
"",
|
||||
"at camera",
|
||||
"away from camera",
|
||||
"other"
|
||||
],
|
||||
"relative_to": ""
|
||||
},
|
||||
"appearance": {
|
||||
"body_build": [
|
||||
"",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"_meta": {
|
||||
"extension": "character"
|
||||
},
|
||||
"character_placement": {
|
||||
"frame_position": [
|
||||
"center",
|
||||
"left",
|
||||
"right",
|
||||
"top",
|
||||
"bottom",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-right"
|
||||
],
|
||||
"proximity_to_camera": [
|
||||
"",
|
||||
"foreground",
|
||||
"midground",
|
||||
"background",
|
||||
"close",
|
||||
"medium",
|
||||
"far"
|
||||
],
|
||||
"body_orientation": [
|
||||
"",
|
||||
"facing camera",
|
||||
"facing left",
|
||||
"facing right",
|
||||
"facing away",
|
||||
"profile left",
|
||||
"profile right",
|
||||
"three-quarter left",
|
||||
"three-quarter right",
|
||||
"reclining",
|
||||
"lying down"
|
||||
],
|
||||
"head_orientation": [
|
||||
"",
|
||||
"facing camera",
|
||||
"facing left",
|
||||
"facing right",
|
||||
"facing up",
|
||||
"facing down",
|
||||
"profile left",
|
||||
"profile right",
|
||||
"tilted left",
|
||||
"tilted right",
|
||||
"turned back"
|
||||
],
|
||||
"looking_at": [
|
||||
"",
|
||||
"at camera",
|
||||
"away from camera",
|
||||
"up",
|
||||
"down",
|
||||
"left",
|
||||
"right",
|
||||
"closed eyes",
|
||||
"other"
|
||||
],
|
||||
"relative_to": ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user