diff --git a/PromptCreatorNode.py b/PromptCreatorNode.py index 1fe7353..93d298c 100644 --- a/PromptCreatorNode.py +++ b/PromptCreatorNode.py @@ -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) diff --git a/__init__.py b/__init__.py index bd2fb98..89936ad 100644 --- a/__init__.py +++ b/__init__.py @@ -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: diff --git a/camera_light.json b/camera_light.json new file mode 100644 index 0000000..24fb99f --- /dev/null +++ b/camera_light.json @@ -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" + } +} diff --git a/daytime.json b/daytime.json new file mode 100644 index 0000000..755df41 --- /dev/null +++ b/daytime.json @@ -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" + } +}