Add v2 skeleton: core/utils/nodes + manifest + DirectorNode stub
This commit is contained in:
@@ -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)",
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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."
|
||||
),
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user