Add v2 skeleton: core/utils/nodes + manifest + DirectorNode stub

This commit is contained in:
zeeoale
2026-02-07 14:45:11 +01:00
parent 1a4e7d24fc
commit 86e8889f2e
8 changed files with 285 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
# DirectorNode.py
# PromptCreatorNode - DirectorNode (0.1-dev)
class DirectorNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"framing": (["extreme_closeup", "closeup", "portrait", "half_body", "full_body"], {"default": "portrait"}),
"camera_mode": (["random", "manual", "lock"], {"default": "random"}),
"camera_index": ("INT", {"default": 0, "min": 0, "max": 200, "step": 1}),
"show_director_preview": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("prompt", "director_preview")
FUNCTION = "direct"
CATEGORY = "PromptCreatorNode/Director"
# A small default camera library (node-owned, not world-owned)
DEFAULT_CAMERA_ANGLES = [
"eye-level portrait, 85mm look",
"three-quarter view, 50mm look",
"slight low angle, 35mm look",
"profile close-up, 105mm look",
"top-down framing, 24mm look",
"centered frontal composition, 70mm look",
]
def direct(self, prompt, framing, camera_mode, camera_index, show_director_preview):
cams = list(self.DEFAULT_CAMERA_ANGLES)
# pick camera
chosen = ""
if camera_mode == "manual":
if cams:
i = max(0, min(int(camera_index), len(cams) - 1))
chosen = cams[i]
elif camera_mode == "lock":
# 0.1-dev: lock behaves like manual (true persistence later)
if cams:
i = max(0, min(int(camera_index), len(cams) - 1))
chosen = cams[i]
else: # random
import random
chosen = random.choice(cams) if cams else ""
# apply framing + camera to prompt
out_prompt = prompt.strip()
if framing:
out_prompt = (out_prompt + ", " if out_prompt else "") + framing.replace("_", " ")
if chosen:
out_prompt = out_prompt + ", " + chosen
# preview
preview = ""
if show_director_preview:
lines = [f"FRAMING: {framing}"]
lines.append(f"CAMERA_MODE: {camera_mode}")
if chosen:
lines.append(f"CHOSEN_CAMERA: {chosen}")
lines.append("CAMERA_LIBRARY:")
for idx, c in enumerate(cams):
marker = "->" if c == chosen else " "
lines.append(f"{marker} {idx}: {c}")
preview = "\n".join(lines)
return (out_prompt, preview)
NODE_CLASS_MAPPINGS = {
"DirectorNode": DirectorNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"DirectorNode": "Director Node (0.1-dev)",
}
+69
View File
@@ -0,0 +1,69 @@
from __future__ import annotations
import random
from typing import Any, Dict, List, Optional
def _pick(arr: Any, fallback: str = "") -> str:
if isinstance(arr, list) and arr:
return str(random.choice(arr))
return fallback
def _join_nonempty(parts: List[str]) -> str:
return ", ".join([p.strip() for p in parts if p and p.strip()])
def build_prompt_from_world(
world: Dict[str, Any],
seed: Optional[int] = None,
custom_intro_index: Optional[int] = None,
) -> str:
if seed is not None:
random.seed(seed)
# 1) system prompt (se ti serve per debug/log o per ritorni futuri)
system_prompt = str(world.get("SYSTEM_PROMPT", "")).strip()
# 2) prendi un custom_intro (se c’è)
custom_intro = ""
ci = world.get("CUSTOM_INTRO")
if isinstance(ci, dict) and ci:
if custom_intro_index is not None and str(custom_intro_index) in ci:
custom_intro = str(ci[str(custom_intro_index)])
else:
# prova 0..6, altrimenti random tra valori
for k in ["0", "1", "2", "3", "4", "5", "6"]:
if k in ci:
custom_intro = str(ci[k])
break
if not custom_intro:
custom_intro = str(random.choice(list(ci.values())))
# 3) selezioni modulari (liste)
outfits = _pick(world.get("OUTFITS"), "simple dark outfit")
lighting = _pick(world.get("LIGHTING"), "low ambient light")
backgrounds = _pick(world.get("BACKGROUNDS"), "intimate interior")
objects = _pick(world.get("OBJECTS"), "personal items")
poses = _pick(world.get("POSES"), "relaxed pose")
expressions = _pick(world.get("EXPRESSIONS"), "calm expression")
camera = _pick(world.get("CAMERA_ANGLES"), "eye-level framing")
atmos = _pick(world.get("ATMOSPHERES"), "quiet cinematic mood")
accessories = _pick(world.get("ACCESSORIES"), "")
# 4) compone una frase unica (poi la raffiniamo nello step successivo)
core = _join_nonempty([
custom_intro,
outfits,
lighting,
backgrounds,
objects,
poses,
expressions,
camera,
atmos,
accessories,
])
# system_prompt lo teniamo separato: qui ritorniamo solo il prompt “artistico”
return core.strip(), system_prompt
+35
View File
@@ -0,0 +1,35 @@
import json
from pathlib import Path
from typing import Any, Dict
def load_world_file(path: Path) -> Dict[str, Any]:
if not path.exists():
raise FileNotFoundError(f"World file not found: {path}")
if path.suffix.lower() != ".json":
raise ValueError(f"World file is not a .json: {path}")
raw = path.read_text(encoding="utf-8")
data = json.loads(raw)
if not isinstance(data, dict):
raise ValueError(f"World JSON must be an object/dict: {path}")
return data
def load_all_worlds_from_dir(dir_path: Path) -> Dict[str, Dict[str, Any]]:
worlds: Dict[str, Dict[str, Any]] = {}
if not dir_path.exists():
return worlds
for p in sorted(dir_path.glob("*.json")):
try:
world = load_world_file(p)
key = world.get("WORLD_NAME") or p.stem
worlds[str(key)] = world
except Exception:
# Qui volutamente silenzioso: il registry/log deciderà se stampare errori
continue
return worlds
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from ..utils.paths import worlds_dir
from ..utils.logger import log
from .world_loader import load_all_worlds_from_dir
@dataclass(frozen=True)
class WorldInfo:
name: str
data: Dict[str, Any]
class WorldRegistry:
_cache: Optional[Dict[str, Dict[str, Any]]] = None
@classmethod
def reload(cls) -> None:
cls._cache = load_all_worlds_from_dir(worlds_dir())
log(f"[PFN] Loaded worlds: {len(cls._cache)} from {worlds_dir()}")
@classmethod
def _ensure_loaded(cls) -> None:
if cls._cache is None:
cls.reload()
@classmethod
def list_names(cls) -> List[str]:
cls._ensure_loaded()
return sorted(cls._cache.keys()) if cls._cache else []
@classmethod
def get(cls, name: str) -> Optional[Dict[str, Any]]:
cls._ensure_loaded()
if not cls._cache:
return None
return cls._cache.get(name)
+10
View File
@@ -0,0 +1,10 @@
MANIFEST = {
"name": "Prompt Creator Node",
"version": (0, 1, 0), # nuova linea di sviluppo
"author": "TK-traumakom",
"project": "https://github.com/traumakom/PromptCreatorNode",
"description": (
"Next-gen prompt suite for ComfyUI: modular worlds, consistent identities, "
"director-style composition, and prompt building utilities."
),
}
+34
View File
@@ -0,0 +1,34 @@
from ..core.world_registry import WorldRegistry
from ..core.prompt_engine import build_prompt_from_world
class DirectorNode:
@classmethod
def INPUT_TYPES(cls):
names = WorldRegistry.list_names()
if not names:
names = ["(no worlds found)"]
return {
"required": {
"world": (names,),
"seed": ("INT", {"default": 0, "min": 0, "max": 2**31 - 1}),
},
"optional": {
"custom_intro_index": ("INT", {"default": -1, "min": -1, "max": 6}),
},
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("prompt", "system_prompt")
FUNCTION = "run"
CATEGORY = "PFN/Director"
def run(self, world: str, seed: int, custom_intro_index: int = -1):
w = WorldRegistry.get(world)
if not w:
return ("(world not found)", "")
idx = None if custom_intro_index < 0 else int(custom_intro_index)
prompt, system_prompt = build_prompt_from_world(w, seed=seed, custom_intro_index=idx)
return (prompt, system_prompt)
+8
View File
@@ -0,0 +1,8 @@
import os
def is_debug() -> bool:
return os.environ.get("PFN_DEBUG", "0") == "1"
def log(msg: str) -> None:
if is_debug():
print(msg)
+10
View File
@@ -0,0 +1,10 @@
from pathlib import Path
def package_root() -> Path:
return Path(__file__).resolve().parents[1]
def data_dir() -> Path:
return package_root() / "data"
def worlds_dir() -> Path:
return data_dir() / "worlds"