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:
+326
-179
@@ -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
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user