DirectorNode: add Shot List (take_count + take_seed_mode + shot_list output)

This commit is contained in:
zeeoale
2026-02-07 17:23:22 +01:00
parent 5090681992
commit 3cf09dfa2d
+146 -83
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
import random
from typing import Dict, Optional
from typing import Dict, Optional, List, Tuple
from ..core.world_registry import WorldRegistry
from ..core.prompt_engine import build_prompt_from_world
@@ -48,11 +48,15 @@ class DirectorNode:
"accessory",
"custom_intro",
],),
# --- R8: Shot List ---
"take_count": ("INT", {"default": 1, "min": 1, "max": 16}),
"take_seed_mode": (["keep", "increment", "randomize"],),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "INT")
RETURN_NAMES = ("prompt", "system_prompt", "final_prompt", "director_notes", "next_seed")
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "INT", "STRING")
RETURN_NAMES = ("prompt", "system_prompt", "final_prompt", "director_notes", "next_seed", "shot_list")
FUNCTION = "run"
CATEGORY = "PFN/Director"
@@ -71,6 +75,49 @@ class DirectorNode:
return {k: v for k, v in data.items() if k in keys and v and str(v).strip()}
def _apply_preset(
self,
director_preset: str,
lock_camera: bool,
lock_lighting: bool,
lock_outfit: bool,
lock_pose: bool,
) -> Tuple[str, bool, bool, bool, bool]:
preset = (director_preset or "custom").lower().strip()
if preset != "custom":
if preset == "continuity":
lock_camera = True
lock_lighting = True
lock_outfit = True
lock_pose = False
elif preset == "exploration":
lock_camera = True
lock_lighting = False
lock_outfit = False
lock_pose = False
elif preset == "chaos":
lock_camera = False
lock_lighting = False
lock_outfit = False
lock_pose = False
return preset, lock_camera, lock_lighting, lock_outfit, lock_pose
def _make_separator(self, separator: str, custom_separator: str) -> str:
sep_raw = custom_separator.strip() if custom_separator and custom_separator.strip() else separator
return sep_raw.encode("utf-8").decode("unicode_escape")
def _compute_next_seed(self, seed: int, lock: bool, mode: str) -> int:
if lock:
return seed
mode = (mode or "keep").lower().strip()
if mode == "increment":
return (seed + 1) % (2**31)
if mode == "randomize":
return random.randint(0, 2**31 - 1)
return seed
def run(
self,
world: str,
@@ -90,105 +137,121 @@ class DirectorNode:
freeze_mode: str,
freeze_scope: str,
change_one: str,
take_count: int,
take_seed_mode: str,
):
w = WorldRegistry.get(world)
if not w:
return ("(world not found)", "", "(world not found)", "", seed)
return ("(world not found)", "", "(world not found)", "", seed, "(world not found)")
# --- validate / normalize ---
effective_seed = int(seed)
take_count = int(take_count)
idx = None
if (custom_intro_mode == "index") and (custom_intro_index is not None) and (int(custom_intro_index) >= 0):
idx = int(custom_intro_index)
# preset
preset = (director_preset or "custom").lower().strip()
if preset != "custom":
if preset == "continuity":
lock_camera = True
lock_lighting = True
lock_outfit = True
lock_pose = False
elif preset == "exploration":
lock_camera = True
lock_lighting = False
lock_outfit = False
lock_pose = False
elif preset == "chaos":
lock_camera = False
lock_lighting = False
lock_outfit = False
lock_pose = False
preset, lock_camera, lock_lighting, lock_outfit, lock_pose = self._apply_preset(
director_preset, lock_camera, lock_lighting, lock_outfit, lock_pose
)
fm = (freeze_mode or "off").lower().strip()
fs = (freeze_scope or "all").lower().strip()
co = (change_one or "none").lower().strip()
sep = self._make_separator(separator, custom_separator)
overrides: Optional[Dict[str, str]] = None
applied_overrides: Dict[str, str] = {}
# --- generate N takes ---
shot_blocks: List[str] = []
current_seed = effective_seed
if fm == "freeze" and self._last:
applied_overrides = self._filter_overrides(self._last, fs)
last_prompt = ""
last_system_prompt = ""
last_final_prompt = ""
last_notes = ""
last_next_seed = current_seed
# Change only one: togli quell'override così viene rigenerato
if co != "none" and co in applied_overrides:
applied_overrides.pop(co, None)
for i in range(take_count):
# freeze overrides (from memory) + change_one
overrides: Optional[Dict[str, str]] = None
applied_overrides: Dict[str, str] = {}
overrides = dict(applied_overrides) if applied_overrides else None
elif fm == "refresh":
overrides = None
else:
overrides = None
if fm == "freeze" and self._last:
applied_overrides = self._filter_overrides(self._last, fs)
prompt, system_prompt, director_notes, chosen = build_prompt_from_world(
w,
seed=effective_seed,
custom_intro_mode=custom_intro_mode,
custom_intro_index=idx,
lock_camera=lock_camera,
lock_lighting=lock_lighting,
lock_outfit=lock_outfit,
lock_pose=lock_pose,
overrides=overrides,
)
# change only one => remove that key from overrides
if co != "none" and co in applied_overrides:
applied_overrides.pop(co, None)
# aggiorna memoria: off/refresh aggiornano sempre; freeze aggiorna SOLO se stai cambiando qualcosa
if fm in ("off", "refresh"):
self._last = dict(chosen)
elif fm == "freeze" and co != "none":
# in freeze con change_one, aggiorniamo la memoria così la nuova take diventa la base
self._last = dict(chosen)
applied_keys = ", ".join(sorted(applied_overrides.keys())) if applied_overrides else "(none)"
header = (
f"preset: {preset}\n"
f"freeze_mode: {fm}\n"
f"freeze_scope: {fs}\n"
f"change_one: {co}\n"
f"applied_overrides: {applied_keys}\n"
)
director_notes = (header + director_notes).strip()
# final prompt
if include_system_prompt:
sp = system_prompt
sep_raw = custom_separator.strip() if custom_separator and custom_separator.strip() else separator
sep = sep_raw.encode("utf-8").decode("unicode_escape")
final_prompt = f"{system_prompt}{sep}{prompt}".strip() if system_prompt else prompt
else:
sp = ""
final_prompt = prompt
# next seed
if lock:
next_seed = effective_seed
else:
mode = (control_after_generate or "keep").lower().strip()
if mode == "increment":
next_seed = (effective_seed + 1) % (2**31)
elif mode == "randomize":
next_seed = random.randint(0, 2**31 - 1)
overrides = dict(applied_overrides) if applied_overrides else None
elif fm == "refresh":
overrides = None
else:
next_seed = effective_seed
overrides = None
return (prompt, sp, final_prompt, director_notes, next_seed)
prompt, system_prompt, director_notes, chosen = build_prompt_from_world(
w,
seed=current_seed,
custom_intro_mode=custom_intro_mode,
custom_intro_index=idx,
lock_camera=lock_camera,
lock_lighting=lock_lighting,
lock_outfit=lock_outfit,
lock_pose=lock_pose,
overrides=overrides,
)
# update memory logic
if fm in ("off", "refresh"):
self._last = dict(chosen)
elif fm == "freeze" and co != "none":
# when changing one, the new take becomes the new base
self._last = dict(chosen)
# final prompt
if include_system_prompt:
sp_out = system_prompt
final_prompt = f"{system_prompt}{sep}{prompt}".strip() if system_prompt else prompt
else:
sp_out = ""
final_prompt = prompt
applied_keys = ", ".join(sorted(applied_overrides.keys())) if applied_overrides else "(none)"
header = (
f"preset: {preset}\n"
f"freeze_mode: {fm}\n"
f"freeze_scope: {fs}\n"
f"change_one: {co}\n"
f"applied_overrides: {applied_keys}\n"
f"take: {i + 1}/{take_count}\n"
f"seed: {current_seed}\n"
)
notes_out = (header + director_notes).strip()
# save last outputs (for node outputs)
last_prompt = prompt
last_system_prompt = sp_out
last_final_prompt = final_prompt
last_notes = notes_out
# shot list block
shot_blocks.append(
"\n".join([
f"===== TAKE {i + 1}/{take_count} =====",
notes_out,
"----- FINAL PROMPT -----",
final_prompt,
"",
]).strip()
)
# compute seed for next take
# (se lock=True, resta fermo; altrimenti segue take_seed_mode)
current_seed = self._compute_next_seed(current_seed, lock, take_seed_mode)
# next_seed for node output should match the same rule of "control_after_generate"
last_next_seed = self._compute_next_seed(effective_seed, lock, control_after_generate)
shot_list = "\n\n".join(shot_blocks).strip()
return (last_prompt, last_system_prompt, last_final_prompt, last_notes, last_next_seed, shot_list)