v1.12.0: add Camera Light and Daytime director layers with JSON control; improve enhancer word handling and lighting stability

This commit is contained in:
zeeoale
2026-02-14 13:42:54 +01:00
parent 4d247afbb5
commit da9882ebfd
4 changed files with 371 additions and 182 deletions
+326 -179
View File
@@ -4,15 +4,19 @@ import json
import requests
from datetime import datetime
class PromptCreatorNode:
NODE_VERSION = "1.11.0"
@classmethod
def INPUT_TYPES(cls):
json_dir = os.path.join(os.path.dirname(__file__), "JSON_DATA")
json_files = [f for f in os.listdir(json_dir) if f.endswith(".json")]
base_path = os.path.dirname(__file__)
json_dir = os.path.join(base_path, "JSON_DATA")
json_files = [f for f in os.listdir(json_dir) if f.endswith(".json")] if os.path.isdir(json_dir) else []
# camera angles (optional) from camera_angles.json
# --- Camera Angles (optional) from camera_angles.json ---
camera_angle_list = ["none"]
camera_angles_path = os.path.join(os.path.dirname(__file__), "camera_angles.json")
camera_angles_path = os.path.join(base_path, "camera_angles.json")
if os.path.exists(camera_angles_path):
try:
with open(camera_angles_path, "r", encoding="utf-8") as f:
@@ -23,17 +27,46 @@ class PromptCreatorNode:
except Exception:
pass
# carico le chiavi dal file esterno system_prompt.json
system_prompt_path = os.path.join(os.path.dirname(__file__), "system_prompt.json")
# --- Camera Light (new) from camera_light.json ---
camera_light_list = ["none"]
camera_light_path = os.path.join(base_path, "camera_light.json")
if os.path.exists(camera_light_path):
try:
with open(camera_light_path, "r", encoding="utf-8") as f:
light_data = json.load(f)
ids = light_data.get("ids", [])
if isinstance(ids, list):
camera_light_list += [str(x) for x in ids if str(x).strip()]
except Exception:
pass
# --- Daytime (new) from daytime.json ---
daytime_list = ["none"]
daytime_path = os.path.join(base_path, "daytime.json")
if os.path.exists(daytime_path):
try:
with open(daytime_path, "r", encoding="utf-8") as f:
dt_data = json.load(f)
ids = dt_data.get("ids", [])
if isinstance(ids, list):
daytime_list += [str(x) for x in ids if str(x).strip()]
except Exception:
pass
# --- system_prompt.json (enhancer modes) ---
system_prompt_path = os.path.join(base_path, "system_prompt.json")
if os.path.exists(system_prompt_path):
with open(system_prompt_path, "r", encoding="utf-8") as f:
system_prompts = json.load(f)
enhancer_modes = list(system_prompts.keys())
try:
with open(system_prompt_path, "r", encoding="utf-8") as f:
system_prompts = json.load(f)
enhancer_modes = list(system_prompts.keys())
except Exception:
enhancer_modes = ["standard"]
else:
enhancer_modes = ["standard"]
# carico le chiavi dal file esterno identities.json (volto consistente)
identities_path = os.path.join(os.path.dirname(__file__), "identities.json")
# --- identities.json ---
identities_path = os.path.join(base_path, "identities.json")
if os.path.exists(identities_path):
try:
with open(identities_path, "r", encoding="utf-8") as f:
@@ -48,56 +81,77 @@ class PromptCreatorNode:
"required": {
"json_name": (sorted(json_files),),
"camera_angle": (camera_angle_list,),
"use_enhancer": (["none", "ollama","llamacpp", "openai", "cohere", "gemini"],),
"camera_light": (camera_light_list,), # ✅ NEW
"daytime": (daytime_list,), # ✅ NEW
"use_enhancer": (["none", "ollama", "llamacpp", "openai", "cohere", "gemini"],),
"enhancer_mode": (enhancer_modes,),
"system_prompt_lock": (["auto", "external"], {"default": "auto"}),
# ✅ NEW: enhancer words controls
"enhancer_words_mode": (["auto", "min", "max"], {"default": "auto"}),
"enhancer_words_min": ("INT", {"default": 140, "min": 40, "max": 400, "step": 5}),
"enhancer_words_max": ("INT", {"default": 220, "min": 40, "max": 600, "step": 5}),
"add_symbols": (["no", "yes"],),
"seed": ("INT", {"default": 0}),
"gender": (["neutral", "female", "2 female", "3 female", "female vampire", "anime woman", "male", "custom"],),
"identity_profile": (identity_profiles,),
"identity_profile": (identity_profiles,),
"external_identity": ("STRING", {"default": "", "multiline": True, "forceInput": True}),
"lock_identity": (["no", "yes"], {"default": "yes"}),
"custom_intro": ("STRING", {"default": "", "multiline": True}),
"custom_intro_id": (["Random","0","1","2","3","4","5","6"], {"default":"Random"}),
"custom_intro_id": (["Random", "0", "1", "2", "3", "4", "5", "6"], {"default": "Random"}),
"horror_intensity": (["auto"] + [str(i) for i in range(11)],),
"sensuality_level": (["auto", "0", "1", "2", "3"],),
"pose_mode": (["random", "world_pick", "lock"], {"default": "random"}),
"pose_index": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1}),
"show_pose_preview": ("BOOLEAN", {"default": True}),
"lora_triggers": ("STRING", {"default": ""}),
"subject_count": (["1", "2", "3"],),
"lock_last_prompt": (["no", "yes"], {"default": "no"}),
"multi_object_count": ("INT", {"default": 1, "min": 1, "max": 5}),
# Ollama settings
"ollama_host": ("STRING", {"default": "http://10.10.10.2:11434"}),
"ollama_model": ("STRING", {"default": "qwen3:8b"})
}
}
RETURN_TYPES = ("STRING", "STRING") # esempio
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("prompt", "pose_preview")
FUNCTION = "generate_prompt"
CATEGORY = "Prompt Creator"
# ---------- Loaders ----------
def _system_prompts(self):
system_prompt_path = os.path.join(os.path.dirname(__file__), "system_prompt.json")
if os.path.exists(system_prompt_path):
with open(system_prompt_path, "r", encoding="utf-8") as f:
return json.load(f)
else:
# fallback se manca il file
return {
"standard": (
"You are a professional prompt enhancer for AI image generation. "
"Return ONE long, multi-clause sentence (~120–160 words), richly descriptive. "
"Cover: genre/style; era; foreground/midground/background; materials & textures; color palette (3–5 tones); "
"lighting (type/direction/intensity); atmosphere; camera angle & lens (mm); composition rule; 3–5 post-process keywords. "
"No story, no dialogue, no lists, no headings."
)
}
try:
with open(system_prompt_path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and data:
return data
except Exception:
pass
# fallback
return {
"standard": (
"You are a professional prompt enhancer for AI image generation. "
"Return ONE long, multi-clause sentence (~120–160 words), richly descriptive. "
"Cover: genre/style; era; foreground/midground/background; materials & textures; color palette (3–5 tones); "
"lighting (type/direction/intensity); atmosphere; camera angle & lens (mm); composition rule; 3–5 post-process keywords. "
"No story, no dialogue, no lists, no headings."
)
}
def _identity_profiles(self):
"""Carica i profili identità dal file identities.json (se presente)."""
identities_path = os.path.join(os.path.dirname(__file__), "identities.json")
if os.path.exists(identities_path):
try:
@@ -109,7 +163,6 @@ class PromptCreatorNode:
return {}
def _identity_to_text(self, identity_obj):
"""Converte un profilo identità in una stringa descrittiva coerente (prompt-only)."""
if not isinstance(identity_obj, dict):
return ""
order = ["age", "face_type", "eyes", "nose", "mouth", "hair", "skin", "expression_base"]
@@ -120,45 +173,66 @@ class PromptCreatorNode:
chunks.append(v.strip())
return ", ".join(chunks)
def _camera_angles(self):
path = os.path.join(os.path.dirname(__file__), "camera_angles.json")
def _load_map_json(self, filename, default_ids=None):
"""
Loads a json shaped like:
{ "ids": [...], "map": { "id": "text", ... } }
"""
base = os.path.dirname(__file__)
path = os.path.join(base, filename)
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
data = json.load(f)
if isinstance(data, dict):
ids = data.get("ids", [])
mp = data.get("map", {})
if not isinstance(ids, list):
ids = []
if not isinstance(mp, dict):
mp = {}
return {"ids": ids, "map": mp}
except Exception:
pass
return {"ids": ["front"], "map": {}}
return {"ids": default_ids or [], "map": {}}
@staticmethod
def _is_none(v):
return (not v) or (str(v).strip().lower() == "none")
def _resolve_camera_angle(self, ui_angle, world_data, angle_dict):
def _resolve_mapped_value(self, ui_value, world_data, mapper_dict, world_keys):
"""
Se UI ha un valore (≠ none) → usa quello e ignora il mondo.
Se UI è none → eredita da world_data["camera_angle"] o ["CAMERA_ANGLE"].
Mappa l'id in camera_angles.map se presente, altrimenti lascia il testo.
Generic resolver:
- If UI value is set (not none) => use it
- Else try world keys in order
- Map id through mapper_dict["map"] if available
"""
if not self._is_none(ui_angle):
chosen = ui_angle
chosen = ""
if not self._is_none(ui_value):
chosen = str(ui_value).strip()
else:
chosen = world_data.get("camera_angle") or world_data.get("CAMERA_ANGLE") or ""
if isinstance(world_data, dict):
for k in world_keys:
v = world_data.get(k)
if isinstance(v, str) and v.strip():
chosen = v.strip()
break
if not chosen:
return ""
mapped = (angle_dict or {}).get("map", {}).get(chosen)
mapped = (mapper_dict or {}).get("map", {}).get(chosen)
return mapped or chosen
def _sanitize_world_camera(self, world_data, ui_angle):
if not self._is_none(ui_angle) and world_data:
def _sanitize_world_keys(self, world_data, ui_value, keys_to_remove):
if not self._is_none(ui_value) and isinstance(world_data, dict):
wd = dict(world_data)
wd.pop("camera_angle", None)
wd.pop("CAMERA_ANGLE", None)
for k in keys_to_remove:
wd.pop(k, None)
return wd
return world_data
# ---------- Prompt builder ----------
def _build_prompt_from_json(
self,
data,
@@ -170,17 +244,20 @@ class PromptCreatorNode:
subject_count,
multi_object_count,
camera_angle_txt="",
camera_light_txt="",
daytime_txt="",
):
parts = []
# 1) Camera angle (se presente) in testa
# 1) Director-level controls in head (order matters)
if isinstance(camera_angle_txt, str) and camera_angle_txt.strip():
parts.append(camera_angle_txt.strip())
if isinstance(camera_light_txt, str) and camera_light_txt.strip():
parts.append(camera_light_txt.strip())
if isinstance(daytime_txt, str) and daytime_txt.strip():
parts.append(daytime_txt.strip())
# 2) Gender / subject intro
# Se gender=custom:
# - se custom_intro (textarea) non è vuoto -> usa quello (override manuale)
# - altrimenti usa custom_intro_id (0..6 o Random) dal world JSON (CUSTOM_INTRO come dict o list)
# 2) Gender / intro
if gender == "custom":
ci = (custom_intro or "").strip()
@@ -190,20 +267,16 @@ class PromptCreatorNode:
if isinstance(world_data, dict):
raw = world_data.get("CUSTOM_INTRO") or world_data.get("custom_intro")
# dict -> keys "0".."6"
if isinstance(raw, dict):
for k in [str(i) for i in range(7)]:
v = raw.get(k)
if isinstance(v, str) and v.strip():
pool[k] = v.strip()
# list -> indices 0..6
elif isinstance(raw, list):
for i in range(min(7, len(raw))):
v = raw[i]
if isinstance(v, str) and v.strip():
pool[str(i)] = v.strip()
return pool
if not ci:
@@ -232,28 +305,25 @@ class PromptCreatorNode:
gender_defaults = {
"male": "a mysterious man",
"female": "a beautiful woman",
"2 female": "two beautiful womans",
"3 female": "three unique beautiful womans",
"2 female": "two beautiful women",
"3 female": "three unique beautiful women",
"female vampire": "a beautiful vampire woman",
"anime woman": "a beautiful anime woman",
}
parts.append(gender_defaults.get(gender, "a striking figure"))
# Optional color realm
color_realm_value = None
if "COLOR_REALM" in data:
if isinstance(data, dict) and "COLOR_REALM" in data:
possible_realms = data["COLOR_REALM"]
if isinstance(possible_realms, list) and possible_realms:
color_realm_value = random.choice(possible_realms)
print(f"[PromptCreator] Using COLOR_REALM: {color_realm_value}")
parts.append(color_realm_value)
multi_keys = ["OBJECTS", "ACCESSORIES"]
# Pick values according to realm or flat lists
if color_realm_value:
if color_realm_value and isinstance(data, dict):
for key in ["OUTFITS", "LIGHTING", "BACKGROUNDS", "OBJECTS", "ACCESSORIES", "ATMOSPHERES"]:
values_by_realm = data.get(key, {}).get(color_realm_value, [])
if isinstance(values_by_realm, list) and values_by_realm:
@@ -263,26 +333,28 @@ class PromptCreatorNode:
else:
parts.append(random.choice(values_by_realm))
else:
for key, values in data.items():
if key.lower() in [
"male", "female", "neutral",
"horror_intensity", "sensuality_level",
"color_realm",
"system_prompt", "world_name",
"custom_intro",
"camera_angles", "camera_angle",
"poses"
]:
continue
if isinstance(values, list) and values:
if key in multi_keys:
sampled = random.sample(values, min(multi_object_count, len(values)))
parts.extend(sampled)
else:
parts.append(random.choice(values))
if isinstance(data, dict):
for key, values in data.items():
if key.lower() in [
"male", "female", "neutral",
"horror_intensity", "sensuality_level",
"color_realm",
"system_prompt", "world_name",
"custom_intro",
"camera_angles", "camera_angle",
"camera_light", "daytime",
"poses"
]:
continue
if isinstance(values, list) and values:
if key in multi_keys:
sampled = random.sample(values, min(multi_object_count, len(values)))
parts.extend(sampled)
else:
parts.append(random.choice(values))
# Horror intensity
if horror_intensity != "auto" and "HORROR_INTENSITY" in data:
if horror_intensity != "auto" and isinstance(data, dict) and "HORROR_INTENSITY" in data:
try:
horror_level = int(horror_intensity)
intensity_entries = data["HORROR_INTENSITY"]
@@ -293,8 +365,8 @@ class PromptCreatorNode:
except ValueError:
pass
# Sensuality level (modulatore simile a HORROR_INTENSITY)
if sensuality_level != "auto" and "SENSUALITY_LEVEL" in data:
# Sensuality level
if sensuality_level != "auto" and isinstance(data, dict) and "SENSUALITY_LEVEL" in data:
try:
s_level = int(sensuality_level)
s_entries = data["SENSUALITY_LEVEL"]
@@ -305,12 +377,12 @@ class PromptCreatorNode:
except ValueError:
pass
if subject_count != "1":
parts.append(f"{subject_count} subjects present")
return ", ".join(parts)
return ", ".join([p for p in parts if isinstance(p, str) and p.strip()])
# ---------- API Keys ----------
def _read_api_keys(self, base_path):
keys_path = os.path.join(base_path, "api_keys.txt")
keys = {}
@@ -322,16 +394,20 @@ class PromptCreatorNode:
keys[k.strip()] = v.strip()
return keys
# ---------- Enhancers ----------
def _enhance_with_ollama(self, host, model, system_prompt, user_prompt):
# Prefer chat to support system role
r = requests.post(f"{host}/api/chat", json={
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
"stream": False
}, timeout=120)
r = requests.post(
f"{host}/api/chat",
json={
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
"stream": False
},
timeout=120
)
r.raise_for_status()
return r.json()["message"]["content"].strip()
@@ -348,23 +424,20 @@ class PromptCreatorNode:
)
return resp.choices[0].message.content.strip()
import requests
def _enhance_with_llamacpp(self, host, system_prompt, user_prompt, temperature=0.7, top_p=0.9, n_predict=220):
"""
llama.cpp server backend.
Tries OpenAI-compatible endpoint first (/v1/chat/completions), then falls back to (/completion).
host example: http://127.0.0.1:11434
"""
host = host.rstrip("/")
host = (host or "").rstrip("/")
# 1) Try OpenAI-compatible chat endpoint
try:
r = requests.post(
f"{host}/v1/chat/completions",
json={
"model": "llama", # ignored by llama.cpp in many builds; kept for compatibility
"model": "llama",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
@@ -379,7 +452,6 @@ class PromptCreatorNode:
if r.status_code == 200:
j = r.json()
# OpenAI-like: choices[0].message.content
content = (
j.get("choices", [{}])[0]
.get("message", {})
@@ -388,8 +460,6 @@ class PromptCreatorNode:
content = (content or "").strip()
if content:
return content
# if non-200, fall through to fallback
except Exception:
pass
@@ -407,14 +477,11 @@ class PromptCreatorNode:
)
r.raise_for_status()
j = r.json()
# llama.cpp /completion commonly returns: {"content": "..."} or {"completion": "..."} depending on version
content = (j.get("content") or j.get("completion") or "").strip()
if not content:
raise RuntimeError(f"llama.cpp returned no content. Keys: {list(j.keys())}")
return content
def _enhance_with_cohere(self, base_path, system_prompt, user_prompt):
import re
try:
@@ -429,29 +496,17 @@ class PromptCreatorNode:
print("[PromptCreator] No Cohere API key found, skipping enhance.")
return user_prompt
# Usa il client V2 (nuova API)
try:
co = cohere.ClientV2(api_key=api_key)
except Exception as e:
print(f"[PromptCreator] Cohere ClientV2 not available: {e}")
return user_prompt
# Messaggi Chat v2
system_msg = (
f"{system_prompt}\n\n"
"You are a prompt enhancer for image generation. "
"Expand the user seed into ONE single, long, flowing sentence "
"with multiple clauses separated by commas and semicolons. "
"Do NOT use bullet points or numbered lists. "
"Keep the original mood and style."
)
user_msg = (
f"Seed: {user_prompt}\n\n"
"Return exactly one long sentence (around 100 to 150 words), no line breaks."
"Return exactly one long sentence (no line breaks)."
)
# === BLOCCO TRY PRINCIPALE ===
try:
resp = co.chat(
model="command-r-08-2024",
@@ -470,23 +525,19 @@ class PromptCreatorNode:
content = " ".join(texts).strip()
if not content:
print("[PromptCreator] Cohere returned empty content.")
return user_prompt
content = re.sub(r"\s*\n+\s*", " ", content).strip()
return content
# === ECCO IL BLOCCO EXCEPT CHE ORA È ALLINEATO PERFETTAMENTE ===
except Exception as e:
print(f"[PromptCreator] Error calling Cohere chat: {e}")
return user_prompt
def _enhance_with_gemini(self, base_path, system_prompt, user_prompt):
import google.generativeai as genai
keys = self._read_api_keys(base_path)
genai.configure(api_key=keys.get("gemini", ""))
# In Gemini, passiamo tutto come testo
model = genai.GenerativeModel("models/gemini-2.5-pro")
txt = (
f"SYSTEM: {system_prompt}\n"
@@ -496,6 +547,7 @@ class PromptCreatorNode:
r = model.generate_content(txt)
return r.candidates[0].content.parts[0].text.strip()
# ---------- Logging ----------
@staticmethod
def log_prompt_run(
json_name,
@@ -505,12 +557,10 @@ class PromptCreatorNode:
lora_triggers,
final_prompt,
source="generated",
node_version="1.5.0",
node_version="1.11.0",
):
"""Append one JSONL entry per run to ./logs/prompt_history.jsonl."""
log_dir = os.path.join(os.path.dirname(__file__), "logs")
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, "prompt_history.jsonl")
entry = {
@@ -528,18 +578,100 @@ class PromptCreatorNode:
with open(log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
# ---------- Enhancer words ----------
@staticmethod
def _build_enhancer_user_prompt(seed_prompt, words_mode, wmin, wmax):
"""
words_mode:
- auto: don't force range
- min : force shorter range around wmin
- max : force longer range up to wmax
"""
base = (seed_prompt or "").strip()
if not base:
return "Enhance this prompt."
# sanitize ints
try:
wmin = int(wmin)
except Exception:
wmin = 140
try:
wmax = int(wmax)
except Exception:
wmax = 220
def generate_prompt(self, json_name, camera_angle, use_enhancer, enhancer_mode, system_prompt_lock, add_symbols, seed, gender, identity_profile, external_identity, lock_identity, custom_intro, custom_intro_id, horror_intensity, sensuality_level, pose_mode, pose_index, show_pose_preview, lora_triggers, subject_count, lock_last_prompt, multi_object_count, ollama_host, ollama_model):
wmin = max(20, min(wmin, 600))
wmax = max(20, min(wmax, 800))
if wmax < wmin:
wmax = wmin
mode = (words_mode or "auto").strip().lower()
if mode == "min":
lo = max(20, wmin - 10)
hi = max(lo + 10, wmin + 20)
return (
f"Enhance this prompt into ONE single long sentence (~{lo}–{hi} words), "
f"no lists and no line breaks: {base}"
)
if mode == "max":
lo = max(30, wmax - 30)
hi = max(lo + 10, wmax)
return (
f"Enhance this prompt into ONE single long sentence (~{lo}–{hi} words), "
f"no lists and no line breaks: {base}"
)
# auto
return f"Enhance this prompt: {base}"
# ---------- Main ----------
def generate_prompt(
self,
json_name,
camera_angle,
camera_light, # ✅ NEW
daytime, # ✅ NEW
use_enhancer,
enhancer_mode,
system_prompt_lock,
enhancer_words_mode, # ✅ NEW
enhancer_words_min, # ✅ NEW
enhancer_words_max, # ✅ NEW
add_symbols,
seed,
gender,
identity_profile,
external_identity,
lock_identity,
custom_intro,
custom_intro_id,
horror_intensity,
sensuality_level,
pose_mode,
pose_index,
show_pose_preview,
lora_triggers,
subject_count,
lock_last_prompt,
multi_object_count,
ollama_host,
ollama_model
):
base_path = os.path.dirname(__file__)
json_path = os.path.join(base_path, "JSON_DATA", json_name)
# history lock
history_path = os.path.join(base_path, "history", f"last_prompt_{json_name}.txt")
history_dir = os.path.join(base_path, "history")
os.makedirs(history_dir, exist_ok=True)
history_path = os.path.join(history_dir, f"last_prompt_{json_name}.txt")
if lock_last_prompt == "yes" and os.path.exists(history_path):
with open(history_path, "r", encoding="utf-8") as f:
prompt = f.read().strip()
pose_preview = "" # best-effort (history has no pose preview)
self.log_prompt_run(
json_name=json_name,
enhancer_mode=enhancer_mode,
@@ -548,17 +680,17 @@ class PromptCreatorNode:
lora_triggers=lora_triggers,
final_prompt=prompt,
source="history_lock",
node_version="1.5.0"
node_version=self.NODE_VERSION
)
print("[PromptCreator] Prompt locked and loaded from history")
return (prompt, )
return (prompt, pose_preview)
try:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
print(f"[PromptCreator] Errore nel caricamento del JSON: {e}")
return ("",)
return ("", "")
# world-level SYSTEM_PROMPT (unified JSON) if present
world_system_prompt = None
@@ -566,16 +698,27 @@ class PromptCreatorNode:
wsp = data.get("SYSTEM_PROMPT") or data.get("system_prompt")
if isinstance(wsp, str) and wsp.strip():
world_system_prompt = wsp.strip()
# Camera angle resolution (UI overrides world)
angle_dict = self._camera_angles()
camera_angle_txt = self._resolve_camera_angle(camera_angle, data, angle_dict)
data = self._sanitize_world_camera(data, camera_angle)
# Seed opzionale per coerenza
# Load director maps
angle_dict = self._load_map_json("camera_angles.json", default_ids=["front"])
light_dict = self._load_map_json("camera_light.json", default_ids=[])
daytime_dict = self._load_map_json("daytime.json", default_ids=[])
# Resolve director values (UI overrides world)
camera_angle_txt = self._resolve_mapped_value(camera_angle, data, angle_dict, ["camera_angle", "CAMERA_ANGLE"])
camera_light_txt = self._resolve_mapped_value(camera_light, data, light_dict, ["camera_light", "CAMERA_LIGHT"])
daytime_txt = self._resolve_mapped_value(daytime, data, daytime_dict, ["daytime", "DAYTIME"])
# Sanitize world keys if overridden by UI
data = self._sanitize_world_keys(data, camera_angle, ["camera_angle", "CAMERA_ANGLE"])
data = self._sanitize_world_keys(data, camera_light, ["camera_light", "CAMERA_LIGHT"])
data = self._sanitize_world_keys(data, daytime, ["daytime", "DAYTIME"])
# Seed
if seed:
random.seed(seed)
# Identity profile (prompt-only consistency)
# Identity profile (prompt-only)
identity_txt = ""
if lock_identity == "yes":
if identity_profile == "external":
@@ -583,32 +726,31 @@ class PromptCreatorNode:
elif identity_profile != "none":
identities = self._identity_profiles()
identity_txt = self._identity_to_text(identities.get(identity_profile, {}))
print("[PromptCreator][DEBUG] identity_profile:", identity_profile)
print("[PromptCreator][DEBUG] lock_identity:", lock_identity)
print("[PromptCreator][DEBUG] identity_txt:", repr(identity_txt))
# Costruisci prompt base
# Build base prompt
prompt = self._build_prompt_from_json(
data, gender, custom_intro, custom_intro_id, horror_intensity, sensuality_level, subject_count, multi_object_count, camera_angle_txt=camera_angle_txt
data=data,
gender=gender,
custom_intro=custom_intro,
custom_intro_id=custom_intro_id,
horror_intensity=horror_intensity,
sensuality_level=sensuality_level,
subject_count=subject_count,
multi_object_count=multi_object_count,
camera_angle_txt=camera_angle_txt,
camera_light_txt=camera_light_txt,
daytime_txt=daytime_txt,
)
# --- POSE CONTROL ---
poses = data.get("POSES") or data.get("poses") or []
# --- POSE CONTROL ---
poses = []
if isinstance(data, dict):
poses = data.get("POSES") or data.get("poses") or []
poses = poses if isinstance(poses, list) else []
pose_preview = ""
chosen_pose = ""
if show_pose_preview and poses:
lines = [f"POSES ({len(poses)}):"]
for i, p in enumerate(poses):
marker = " ->" if (chosen_pose and str(p).strip() == chosen_pose) else " "
lines.append(f"{marker} {i}: {p}")
pose_preview = "\n".join(lines)
elif show_pose_preview:
pose_preview = "POSES: (none found in this world JSON)"
pose_path = os.path.join(base_path, "history", f"last_pose_{json_name}.txt")
pose_path = os.path.join(history_dir, f"last_pose_{json_name}.txt")
def pick_pose_by_index(i):
if not poses:
@@ -616,47 +758,52 @@ class PromptCreatorNode:
i = max(0, min(int(i), len(poses) - 1))
return str(poses[i]).strip()
if pose_mode == "world_pick":
chosen_pose = pick_pose_by_index(pose_index)
elif pose_mode == "lock":
if os.path.exists(pose_path):
with open(pose_path, "r", encoding="utf-8") as f:
chosen_pose = f.read().strip()
if not chosen_pose:
chosen_pose = pick_pose_by_index(pose_index)
else: # random
if poses:
chosen_pose = random.choice(poses).strip()
if chosen_pose:
prompt = prompt + ", " + chosen_pose
os.makedirs(os.path.dirname(pose_path), exist_ok=True)
with open(pose_path, "w", encoding="utf-8") as f:
f.write(chosen_pose)
# --- END POSE CONTROL ---
if show_pose_preview:
if poses:
lines = [f"POSES ({len(poses)}):"]
for i, p in enumerate(poses):
marker = " ->" if (chosen_pose and str(p).strip() == chosen_pose) else " "
lines.append(f"{marker} {i}: {p}")
pose_preview = "\n".join(lines)
else:
pose_preview = "POSES: (none found in this world JSON)"
# Identity appended after pose (keeps face consistency late in chain)
if identity_txt:
prompt = prompt + ", " + identity_txt
print("[PromptCreator][DEBUG] prompt_pre_enhancer:", prompt)
# System prompt condiviso per tutti i backend
# System prompt selection
system_prompts = self._system_prompts()
effective_world_system_prompt = None if system_prompt_lock == "external" else world_system_prompt
system_prompt = effective_world_system_prompt or system_prompts.get(enhancer_mode, system_prompts["standard"])
user_prompt = f"Enhance this prompt: {prompt}"
system_prompt = effective_world_system_prompt or system_prompts.get(enhancer_mode, system_prompts.get("standard", ""))
# ✅ NEW: words-controlled user prompt for enhancer
user_prompt = self._build_enhancer_user_prompt(
seed_prompt=prompt,
words_mode=enhancer_words_mode,
wmin=enhancer_words_min,
wmax=enhancer_words_max
)
# Enhancer backends
if use_enhancer != "none":
print("[PromptCreator][DEBUG] use_enhancer:", use_enhancer)
print("[PromptCreator][DEBUG] enhancer_mode:", enhancer_mode)
print("[PromptCreator][DEBUG] system_prompt:", system_prompt)
print("[PromptCreator][DEBUG] user_prompt:", user_prompt)
try:
if use_enhancer == "ollama":
prompt = self._enhance_with_ollama(ollama_host, ollama_model, system_prompt, user_prompt)
@@ -672,20 +819,18 @@ class PromptCreatorNode:
print(f"[PromptCreator] Errore nell'enhancer ({use_enhancer}): {e}")
# LoRA triggers
if lora_triggers.strip():
if isinstance(lora_triggers, str) and lora_triggers.strip():
lts = [x.strip() for x in lora_triggers.split(",") if x.strip()]
if lts:
prompt += ", " + ", ".join(lts)
print(f"[PromptCreator] LoRA triggers aggiunti: {lts}")
if add_symbols == "yes":
prompt = f"[{prompt}]"
os.makedirs(os.path.join(base_path, "history"), exist_ok=True)
# Persist history
with open(history_path, "w", encoding="utf-8") as f:
f.write(prompt.strip())
print(f"[PromptCreator] Prompt finale: {prompt}")
self.log_prompt_run(
json_name=json_name,
enhancer_mode=enhancer_mode,
@@ -694,6 +839,8 @@ class PromptCreatorNode:
lora_triggers=lora_triggers,
final_prompt=prompt,
source="generated",
node_version="1.5.0"
node_version=self.NODE_VERSION
)
print(f"[PromptCreator] Prompt finale: {prompt}")
return (prompt, pose_preview)
+3 -3
View File
@@ -4,7 +4,7 @@
MANIFEST = {
"name": "Prompt Creator Node",
"version": (1, 11, 0),
"version": (1, 12, 0),
"author": "TK-traumakom",
"project": "https://github.com/zeeoale/PromptCreatorNode",
"description": (
@@ -39,11 +39,11 @@ def _print_promptcreator_banner():
██ ██ ██ ██ ██ ██ ██ ██
██ ████ ██████ ██████ ███████
-----------------------------------------------------
1.11.0
1.12.0
-----------------------------------------------------
===========================================================
PromptCreatorNode v1.11.0
PromptCreatorNode v1.12.0
A modular cinematic prompt generator for ComfyUI
Loaded nodes:
+22
View File
@@ -0,0 +1,22 @@
{
"ids": [
"soft_key",
"hard_key",
"rim_light",
"top_light",
"practical_mixed",
"moonlit_ambient",
"firelight_accent",
"soft_bloom_diffusion"
],
"map": {
"soft_key": "soft diffused key light, gentle falloff and natural skin rolloff",
"hard_key": "hard directional key light, crisp shadows and high micro-contrast",
"rim_light": "thin rim light separating subject from background, subtle edge highlights",
"top_light": "soft top light shaping forehead and cheekbones, controlled shadow depth",
"practical_mixed": "mixed practical lighting, imperfect and realistic, subtle uneven exposure",
"moonlit_ambient": "cool moonlit ambient illumination, minimal catchlight, restrained contrast",
"firelight_accent": "warm firelight as secondary accent, physically plausible flicker and soft bloom",
"soft_bloom_diffusion": "cinematic diffusion with restrained bloom, highlights softened without fantasy glow"
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"ids": [
"dawn",
"morning",
"midday",
"golden_hour",
"blue_hour",
"night",
"overcast"
],
"map": {
"dawn": "early dawn, pale sky, cool low-angle light and quiet haze",
"morning": "clear morning light, fresh air, gentle contrast and clean highlights",
"midday": "bright midday light, neutral balance, minimal long shadows and crisp clarity",
"golden_hour": "golden hour, warm low sun, long soft shadows and gentle glow",
"blue_hour": "blue hour twilight, cool ambient tones, soft silhouettes and calm atmosphere",
"night": "deep night, low ambient visibility, restrained highlights and quiet darkness",
"overcast": "overcast daylight, diffused soft illumination, muted contrast and subdued palette"
}
}