update To V2
This commit is contained in:
+70
-2063
File diff suppressed because it is too large
Load Diff
+24
@@ -10,8 +10,16 @@ from .NoxPromptNode import (
|
||||
NoxPromptBuilder,
|
||||
NoxPromptPaletteMixer,
|
||||
NoxPromptNarrativeWeaver,
|
||||
NoxPromptCharacterCreator,
|
||||
NoxPromptHumanDesigner,
|
||||
NoxPromptWardrobeDesigner,
|
||||
NoxPromptActionDirector,
|
||||
NoxPromptLightingMaster,
|
||||
NoxPromptNSFWActionDirector,
|
||||
NoxPromptNSFWDesigner,
|
||||
NoxPromptCamz,
|
||||
NoxPromptCombiner,
|
||||
NoxPromptPipelineCombiner,
|
||||
NoxPromptAnalyzer,
|
||||
)
|
||||
|
||||
@@ -21,8 +29,16 @@ NODE_CLASS_MAPPINGS = {
|
||||
"NoxPromptBuilder": NoxPromptBuilder,
|
||||
"NoxPromptPaletteMixer": NoxPromptPaletteMixer,
|
||||
"NoxPromptNarrativeWeaver": NoxPromptNarrativeWeaver,
|
||||
"NoxPromptCharacterCreator": NoxPromptCharacterCreator,
|
||||
"NoxPromptHumanDesigner": NoxPromptHumanDesigner,
|
||||
"NoxPromptWardrobeDesigner": NoxPromptWardrobeDesigner,
|
||||
"NoxPromptActionDirector": NoxPromptActionDirector,
|
||||
"NoxPromptLightingMaster": NoxPromptLightingMaster,
|
||||
"NoxPromptNSFWActionDirector": NoxPromptNSFWActionDirector,
|
||||
"NoxPromptNSFWDesigner": NoxPromptNSFWDesigner,
|
||||
"NoxPromptCamz": NoxPromptCamz,
|
||||
"NoxPromptCombiner": NoxPromptCombiner,
|
||||
"NoxPromptPipelineCombiner": NoxPromptPipelineCombiner,
|
||||
"NoxPromptAnalyzer": NoxPromptAnalyzer,
|
||||
}
|
||||
|
||||
@@ -32,8 +48,16 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"NoxPromptBuilder": "Nox Prompt Builder",
|
||||
"NoxPromptPaletteMixer": "Nox Prompt Palette Mixer",
|
||||
"NoxPromptNarrativeWeaver": "Nox Prompt Narrative Weaver",
|
||||
"NoxPromptCharacterCreator": "Nox Prompt Character Creator",
|
||||
"NoxPromptHumanDesigner": "Nox Prompt Human Designer",
|
||||
"NoxPromptWardrobeDesigner": "Nox Prompt Wardrobe Designer",
|
||||
"NoxPromptActionDirector": "Nox Prompt Action Director",
|
||||
"NoxPromptLightingMaster": "Nox Prompt Lighting Master",
|
||||
"NoxPromptNSFWActionDirector": "Nox Prompt NSFW Action Director",
|
||||
"NoxPromptNSFWDesigner": "Nox Prompt NSFW Designer",
|
||||
"NoxPromptCamz": "Nox Camz",
|
||||
"NoxPromptCombiner": "Nox Prompt Combiner",
|
||||
"NoxPromptPipelineCombiner": "Nox Prompt Pipeline Combiner",
|
||||
"NoxPromptAnalyzer": "Nox Prompt Analyzer",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Modular Nox Prompter node package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import common as _common
|
||||
from . import constants as _constants
|
||||
from .nodes import NODE_CLASSES as _NODE_CLASSES
|
||||
from .nodes import get_node_classes as _get_node_classes
|
||||
|
||||
|
||||
NODE_CLASSES = tuple(_NODE_CLASSES)
|
||||
get_node_classes = _get_node_classes
|
||||
|
||||
for _helper_name in (
|
||||
"PresetManager",
|
||||
"PresetMixin",
|
||||
"PromptFragmentFilter",
|
||||
"_format_notes",
|
||||
"_resolve_action_option",
|
||||
"_resolve_option",
|
||||
"_resolve_with_custom",
|
||||
"_split_tokens",
|
||||
):
|
||||
globals()[_helper_name] = getattr(_common, _helper_name)
|
||||
|
||||
for _cls in NODE_CLASSES:
|
||||
globals()[_cls.__name__] = _cls
|
||||
|
||||
for _const_name in dir(_constants):
|
||||
if _const_name.isupper():
|
||||
globals()[_const_name] = getattr(_constants, _const_name)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,325 @@
|
||||
"""Common utilities shared across Nox Prompter nodes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from .constants import CUSTOM_OPTION, PRESET_ROOT, PROMPT_CONTRADICTION_RULES
|
||||
|
||||
|
||||
class PromptFragmentFilter:
|
||||
"""Organize, deduplicate, and lightly curate prompt fragments."""
|
||||
|
||||
PROFILE_PRESETS = {
|
||||
"balanced": {
|
||||
"remove": {"placeholder", "generic"},
|
||||
"max_fragments": 18,
|
||||
"priority": ["subject", "scene", "motion", "camera", "style", "extra"],
|
||||
"keywords": {
|
||||
"motion": ["motion", "camera", "move", "tempo"],
|
||||
"style": ["style", "palette", "effect", "lighting"],
|
||||
"scene": ["scene", "environment", "setting", "location"],
|
||||
"subject": ["subject", "portrait", "hero", "character"],
|
||||
"camera": ["lens", "framing", "focus"],
|
||||
},
|
||||
"insight": "Balanced profile organizes fragments from subject through stylistic flourishes.",
|
||||
},
|
||||
"cinematic": {
|
||||
"remove": {"generic", "boring", "placeholder"},
|
||||
"max_fragments": 20,
|
||||
"priority": ["subject", "scene", "camera", "motion", "style", "extra"],
|
||||
"keywords": {
|
||||
"camera": ["camera", "lens", "framing", "cinematic"],
|
||||
"style": ["film", "cinematic", "grade", "bokeh"],
|
||||
"scene": ["scene", "set", "location", "environment"],
|
||||
"motion": ["move", "motion", "tempo", "beat"],
|
||||
"subject": ["subject", "hero", "character", "actor"],
|
||||
},
|
||||
"insight": "Cinematic profile spotlights camera language alongside scene context.",
|
||||
},
|
||||
"minimal": {
|
||||
"remove": {"redundant", "generic", "placeholder"},
|
||||
"max_fragments": 12,
|
||||
"priority": ["subject", "scene", "style", "extra"],
|
||||
"keywords": {
|
||||
"style": ["palette", "style", "mood", "tone"],
|
||||
"scene": ["scene", "environment", "location"],
|
||||
"subject": ["subject", "portrait", "focus"],
|
||||
},
|
||||
"insight": "Minimal profile trims to the essentials for shorter base prompts.",
|
||||
},
|
||||
"narrative": {
|
||||
"remove": {"placeholder"},
|
||||
"max_fragments": 22,
|
||||
"priority": ["subject", "scene", "motion", "style", "extra"],
|
||||
"keywords": {
|
||||
"motion": ["motion", "beat", "arc", "tempo"],
|
||||
"scene": ["scene", "setting", "environment", "beat"],
|
||||
"subject": ["subject", "character", "hero", "protagonist"],
|
||||
"style": ["tone", "mood", "style", "palette"],
|
||||
},
|
||||
"insight": "Narrative profile preserves motion arcs and tonal descriptors for storytelling prompts.",
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, profile: str = "balanced") -> None:
|
||||
self.profile_name = (profile or "balanced").lower()
|
||||
self.profile = self.PROFILE_PRESETS.get(self.profile_name, self.PROFILE_PRESETS["balanced"])
|
||||
self._removed: List[str] = []
|
||||
|
||||
def organize(self, fragments: List[str]) -> List[str]:
|
||||
prioritized: List[Tuple[str, str]] = []
|
||||
seen = set()
|
||||
for fragment in fragments:
|
||||
cleaned = self._normalize(fragment)
|
||||
if not cleaned:
|
||||
continue
|
||||
lowered = cleaned.lower()
|
||||
if any(token in lowered for token in self.profile.get("remove", [])):
|
||||
self._removed.append(cleaned)
|
||||
continue
|
||||
if lowered in seen:
|
||||
continue
|
||||
seen.add(lowered)
|
||||
bucket = self._bucket_for(cleaned)
|
||||
prioritized.append((bucket, cleaned))
|
||||
|
||||
priority_order = {label: index for index, label in enumerate(self.profile.get("priority", []))}
|
||||
prioritized.sort(key=lambda item: (priority_order.get(item[0], len(priority_order)), item[1]))
|
||||
limited = prioritized[: self.profile.get("max_fragments", len(prioritized))]
|
||||
return [item[1] for item in limited]
|
||||
|
||||
def summarize(self) -> str:
|
||||
insight = self.profile.get("insight", "Prompt fragments were harmonized.")
|
||||
if self._removed:
|
||||
removed = ", ".join(self._removed[:3])
|
||||
if len(self._removed) > 3:
|
||||
removed += ", ..."
|
||||
return f"{insight} Removed duplicates/noise: {removed}."
|
||||
return insight
|
||||
|
||||
def _normalize(self, fragment: str) -> str:
|
||||
cleaned = (fragment or "").strip()
|
||||
cleaned = re.sub(r"\s+", " ", cleaned)
|
||||
return cleaned
|
||||
|
||||
def _bucket_for(self, fragment: str) -> str:
|
||||
lowered = fragment.lower()
|
||||
keyword_map = self.profile.get("keywords", {})
|
||||
for bucket, keywords in keyword_map.items():
|
||||
if any(keyword in lowered for keyword in keywords):
|
||||
return bucket
|
||||
if lowered.startswith("camera"):
|
||||
return "camera"
|
||||
if lowered.startswith("palette"):
|
||||
return "style"
|
||||
return "extra"
|
||||
|
||||
|
||||
def _sanitize_preset_name(name: str) -> str:
|
||||
normalized = (name or "").strip()
|
||||
if not normalized:
|
||||
return ""
|
||||
return re.sub(r"[^0-9A-Za-z._-]+", "_", normalized)
|
||||
|
||||
|
||||
class PresetManager:
|
||||
"""Simple JSON-based preset storage scoped per node."""
|
||||
|
||||
def __init__(self, base_dir: str = PRESET_ROOT):
|
||||
self.base_dir = base_dir
|
||||
|
||||
def _node_dir(self, node_key: str, ensure: bool = False) -> str:
|
||||
path = os.path.join(self.base_dir, node_key)
|
||||
if ensure:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
def save(self, node_key: str, preset_name: str, data: Dict[str, Any]) -> str:
|
||||
sanitized = _sanitize_preset_name(preset_name)
|
||||
if not sanitized:
|
||||
raise ValueError("Preset name is required to save.")
|
||||
directory = self._node_dir(node_key, ensure=True)
|
||||
file_path = os.path.join(directory, f"{sanitized}.json")
|
||||
with open(file_path, "w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, indent=2, ensure_ascii=False)
|
||||
return file_path
|
||||
|
||||
def load(self, node_key: str, preset_name: str) -> Dict[str, Any]:
|
||||
sanitized = _sanitize_preset_name(preset_name)
|
||||
if not sanitized:
|
||||
raise ValueError("Preset name is required to load.")
|
||||
directory = self._node_dir(node_key)
|
||||
file_path = os.path.join(directory, f"{sanitized}.json")
|
||||
if not os.path.isfile(file_path):
|
||||
raise FileNotFoundError(f"Preset '{preset_name}' not found for {node_key}.")
|
||||
with open(file_path, "r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
def list(self, node_key: str) -> List[str]:
|
||||
directory = self._node_dir(node_key)
|
||||
if not os.path.isdir(directory):
|
||||
return []
|
||||
names = []
|
||||
for entry in os.listdir(directory):
|
||||
if entry.lower().endswith(".json"):
|
||||
names.append(os.path.splitext(entry)[0])
|
||||
return sorted(names)
|
||||
|
||||
|
||||
class PresetMixin:
|
||||
_preset_manager = PresetManager()
|
||||
|
||||
def _apply_preset_action(
|
||||
self,
|
||||
node_key: str,
|
||||
preset_action: str,
|
||||
preset_name: str,
|
||||
data: Dict[str, Any],
|
||||
) -> Tuple[Dict[str, Any], str]:
|
||||
action = (preset_action or "none").strip().lower()
|
||||
name = (preset_name or "").strip()
|
||||
status_messages: List[str] = []
|
||||
result = dict(data)
|
||||
|
||||
try:
|
||||
if action == "save":
|
||||
path = self._preset_manager.save(node_key, name, result)
|
||||
rel_path = os.path.relpath(path, os.path.dirname(__file__))
|
||||
status_messages.append(f"Preset saved: {rel_path}")
|
||||
elif action == "load":
|
||||
loaded = self._preset_manager.load(node_key, name)
|
||||
for key, value in loaded.items():
|
||||
if key in result:
|
||||
result[key] = value
|
||||
status_messages.append(f"Preset loaded: {name}")
|
||||
elif action == "list":
|
||||
available = self._preset_manager.list(node_key)
|
||||
if available:
|
||||
status_messages.append("Available presets: " + ", ".join(available))
|
||||
else:
|
||||
status_messages.append("No presets saved yet.")
|
||||
except FileNotFoundError as exc:
|
||||
status_messages.append(str(exc))
|
||||
except json.JSONDecodeError as exc:
|
||||
status_messages.append(f"Preset JSON invalid: {exc}")
|
||||
except ValueError as exc:
|
||||
status_messages.append(str(exc))
|
||||
except OSError as exc: # pragma: no cover
|
||||
status_messages.append(f"Preset action failed: {exc}")
|
||||
|
||||
status = " | ".join(status_messages)
|
||||
return result, status
|
||||
|
||||
|
||||
def _resolve_action_option(
|
||||
selection: str,
|
||||
custom_prompt: str,
|
||||
table: Dict[str, Dict[str, str]],
|
||||
) -> Tuple[str, str]:
|
||||
if selection == CUSTOM_OPTION:
|
||||
value = (custom_prompt or "").strip()
|
||||
return value, ""
|
||||
record = table.get(selection)
|
||||
if record:
|
||||
return record.get("prompt", ""), record.get("summary", "")
|
||||
fallback = (selection or "").strip()
|
||||
return fallback, ""
|
||||
|
||||
|
||||
def _resolve_option(selection: str, table: Dict[str, Dict[str, str]]) -> Tuple[str, str]:
|
||||
record = table.get(selection)
|
||||
if record:
|
||||
return record.get("prompt", ""), record.get("notes", record.get("summary", ""))
|
||||
fallback = (selection or "").strip()
|
||||
return fallback, ""
|
||||
|
||||
|
||||
def _split_tokens(value: str) -> List[str]:
|
||||
if not value:
|
||||
return []
|
||||
tokens: List[str] = []
|
||||
for chunk in value.replace("\r", "\n").split("\n"):
|
||||
parts = [token.strip() for token in chunk.split(",") if token.strip()]
|
||||
tokens.extend(parts)
|
||||
return tokens
|
||||
|
||||
|
||||
def _resolve_with_custom(
|
||||
selection: str,
|
||||
custom_value: str,
|
||||
table: Dict[str, Dict[str, str]],
|
||||
) -> Tuple[str, str]:
|
||||
if selection == CUSTOM_OPTION:
|
||||
value = (custom_value or "").strip()
|
||||
return value, "Custom selection provided."
|
||||
prompt, notes = _resolve_option(selection, table)
|
||||
return prompt, notes
|
||||
|
||||
|
||||
def _format_notes(label: str, notes: str, fallback: str) -> str:
|
||||
content = (notes or fallback or "").strip()
|
||||
if not content:
|
||||
return ""
|
||||
return f"{label}: {content}" if label else content
|
||||
|
||||
|
||||
def detect_prompt_contradictions(chunks: List[str]) -> List[str]:
|
||||
"""Identify conflicting descriptors across provided prompt fragments."""
|
||||
|
||||
if not chunks:
|
||||
return []
|
||||
|
||||
lowered_sources: List[str] = []
|
||||
for chunk in chunks:
|
||||
cleaned = (chunk or "").strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
lowered_sources.append(cleaned.lower())
|
||||
|
||||
warnings: List[str] = []
|
||||
|
||||
for rule in PROMPT_CONTRADICTION_RULES:
|
||||
groups = rule.get("groups", {})
|
||||
detected: Dict[str, List[str]] = {}
|
||||
for group_name, keywords in groups.items():
|
||||
for keyword in keywords:
|
||||
needle = keyword.lower().strip()
|
||||
if not needle:
|
||||
continue
|
||||
for source in lowered_sources:
|
||||
if needle in source:
|
||||
detected.setdefault(group_name, [])
|
||||
if keyword not in detected[group_name]:
|
||||
detected[group_name].append(keyword)
|
||||
break
|
||||
if len(detected) > 1:
|
||||
fragments = []
|
||||
for group_name, matches in detected.items():
|
||||
sample = ", ".join(matches[:3])
|
||||
if len(matches) > 3:
|
||||
sample += ", ..."
|
||||
fragments.append(f"{group_name}: {sample}")
|
||||
message = f"{rule.get('label', 'Consistency')}: " + " | ".join(fragments)
|
||||
hint = rule.get("hint")
|
||||
if hint:
|
||||
message += f" ({hint})"
|
||||
warnings.append(message)
|
||||
|
||||
return warnings
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PromptFragmentFilter",
|
||||
"PresetManager",
|
||||
"PresetMixin",
|
||||
"_resolve_action_option",
|
||||
"_resolve_option",
|
||||
"_resolve_with_custom",
|
||||
"_split_tokens",
|
||||
"_format_notes",
|
||||
"detect_prompt_contradictions",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
"""Node modules for the modular Nox Prompter package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .nox_prompt_action_director import NoxPromptActionDirector
|
||||
from .nox_prompt_analyzer import NoxPromptAnalyzer
|
||||
from .nox_prompt_builder import NoxPromptBuilder
|
||||
from .nox_prompt_camz import NoxPromptCamz
|
||||
from .nox_prompt_character_creator import NoxPromptCharacterCreator
|
||||
from .nox_prompt_combiner import NoxPromptCombiner
|
||||
from .nox_prompt_enhancer import NoxPromptEnhancer
|
||||
from .nox_prompt_human_designer import NoxPromptHumanDesigner
|
||||
from .nox_prompt_lighting_master import NoxPromptLightingMaster
|
||||
from .nox_prompt_narrative_weaver import NoxPromptNarrativeWeaver
|
||||
from .nox_prompt_nsfw_action_director import NoxPromptNSFWActionDirector
|
||||
from .nox_prompt_nsfw_designer import NoxPromptNSFWDesigner
|
||||
from .nox_prompt_palette_mixer import NoxPromptPaletteMixer
|
||||
from .nox_prompt_pipeline_combiner import NoxPromptPipelineCombiner
|
||||
from .nox_prompt_wardrobe_designer import NoxPromptWardrobeDesigner
|
||||
from .nox_prompt_usage_guide import NoxPromptUsageGuide
|
||||
from .nox_prompt_prompt_organizer import NoxPromptOrganizer
|
||||
from .nox_prompt_nsfw_aio import NoxPromptNSFWAIO
|
||||
|
||||
_NODE_CLASSES = (
|
||||
NoxPromptUsageGuide,
|
||||
NoxPromptOrganizer,
|
||||
NoxPromptEnhancer,
|
||||
NoxPromptBuilder,
|
||||
NoxPromptPaletteMixer,
|
||||
NoxPromptNarrativeWeaver,
|
||||
NoxPromptCharacterCreator,
|
||||
NoxPromptHumanDesigner,
|
||||
NoxPromptWardrobeDesigner,
|
||||
NoxPromptActionDirector,
|
||||
NoxPromptNSFWActionDirector,
|
||||
NoxPromptNSFWAIO,
|
||||
NoxPromptLightingMaster,
|
||||
NoxPromptNSFWDesigner,
|
||||
NoxPromptCamz,
|
||||
NoxPromptCombiner,
|
||||
NoxPromptPipelineCombiner,
|
||||
NoxPromptAnalyzer,
|
||||
)
|
||||
|
||||
NODE_CLASSES = _NODE_CLASSES
|
||||
|
||||
|
||||
def get_node_classes():
|
||||
"""Return all node classes defined in the modular package."""
|
||||
|
||||
return NODE_CLASSES
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NoxPromptUsageGuide",
|
||||
"NoxPromptOrganizer",
|
||||
"NoxPromptEnhancer",
|
||||
"NoxPromptBuilder",
|
||||
"NoxPromptPaletteMixer",
|
||||
"NoxPromptNarrativeWeaver",
|
||||
"NoxPromptCharacterCreator",
|
||||
"NoxPromptHumanDesigner",
|
||||
"NoxPromptWardrobeDesigner",
|
||||
"NoxPromptActionDirector",
|
||||
"NoxPromptNSFWActionDirector",
|
||||
"NoxPromptNSFWAIO",
|
||||
"NoxPromptLightingMaster",
|
||||
"NoxPromptNSFWDesigner",
|
||||
"NoxPromptCamz",
|
||||
"NoxPromptCombiner",
|
||||
"NoxPromptPipelineCombiner",
|
||||
"NoxPromptAnalyzer",
|
||||
"NODE_CLASSES",
|
||||
"get_node_classes",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,193 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin, _resolve_action_option, _split_tokens
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptActionDirector(PresetMixin):
|
||||
"""Choreograph dynamic action beats, tempo, and supporting notes."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"primary_action": (list(ACTION_PRIMARY_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Heroic Sprint"}),
|
||||
},
|
||||
"optional": {
|
||||
"secondary_action": (list(ACTION_SECONDARY_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "None"}),
|
||||
"tempo": (list(ACTION_TEMPO_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Rising Momentum"}),
|
||||
"interaction_mode": (list(ACTION_INTERACTION_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Solo Focus"}),
|
||||
"props": ("STRING", {"multiline": True, "default": ""}),
|
||||
"environment_beat": ("STRING", {"multiline": True, "default": ""}),
|
||||
"intensity_descriptor": ("STRING", {"default": ""}),
|
||||
"camera_language": ("STRING", {"multiline": True, "default": ""}),
|
||||
"sound_design": ("STRING", {"multiline": True, "default": ""}),
|
||||
"dialogue_hook": ("STRING", {"multiline": True, "default": ""}),
|
||||
"emotional_anchor": ("STRING", {"default": ""}),
|
||||
"risk_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"beat_emphasis": ("STRING", {"multiline": True, "default": ""}),
|
||||
"primary_action_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"secondary_action_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tempo_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"interaction_mode_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"primary_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"secondary_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tempo_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"interaction_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("action_prompt", "action_summary", "director_notes", "preset_status")
|
||||
FUNCTION = "direct"
|
||||
CATEGORY = "NoxPrompter/Action"
|
||||
|
||||
def direct(
|
||||
self,
|
||||
primary_action,
|
||||
secondary_action="None",
|
||||
tempo="Rising Momentum",
|
||||
interaction_mode="Solo Focus",
|
||||
props="",
|
||||
environment_beat="",
|
||||
intensity_descriptor="",
|
||||
camera_language="",
|
||||
sound_design="",
|
||||
dialogue_hook="",
|
||||
emotional_anchor="",
|
||||
risk_notes="",
|
||||
beat_emphasis="",
|
||||
primary_action_custom="",
|
||||
secondary_action_custom="",
|
||||
tempo_custom="",
|
||||
interaction_mode_custom="",
|
||||
primary_notes="",
|
||||
secondary_notes="",
|
||||
tempo_notes="",
|
||||
interaction_notes="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"primary_action": primary_action,
|
||||
"secondary_action": secondary_action,
|
||||
"tempo": tempo,
|
||||
"interaction_mode": interaction_mode,
|
||||
"props": props,
|
||||
"environment_beat": environment_beat,
|
||||
"intensity_descriptor": intensity_descriptor,
|
||||
"camera_language": camera_language,
|
||||
"sound_design": sound_design,
|
||||
"dialogue_hook": dialogue_hook,
|
||||
"emotional_anchor": emotional_anchor,
|
||||
"risk_notes": risk_notes,
|
||||
"beat_emphasis": beat_emphasis,
|
||||
"primary_action_custom": primary_action_custom,
|
||||
"secondary_action_custom": secondary_action_custom,
|
||||
"tempo_custom": tempo_custom,
|
||||
"interaction_mode_custom": interaction_mode_custom,
|
||||
"primary_notes": primary_notes,
|
||||
"secondary_notes": secondary_notes,
|
||||
"tempo_notes": tempo_notes,
|
||||
"interaction_notes": interaction_notes,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"action_director",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
primary_action = config.get("primary_action", primary_action)
|
||||
secondary_action = config.get("secondary_action", secondary_action)
|
||||
tempo = config.get("tempo", tempo)
|
||||
interaction_mode = config.get("interaction_mode", interaction_mode)
|
||||
props = config.get("props", props)
|
||||
environment_beat = config.get("environment_beat", environment_beat)
|
||||
intensity_descriptor = config.get("intensity_descriptor", intensity_descriptor)
|
||||
camera_language = config.get("camera_language", camera_language)
|
||||
sound_design = config.get("sound_design", sound_design)
|
||||
dialogue_hook = config.get("dialogue_hook", dialogue_hook)
|
||||
emotional_anchor = config.get("emotional_anchor", emotional_anchor)
|
||||
risk_notes = config.get("risk_notes", risk_notes)
|
||||
beat_emphasis = config.get("beat_emphasis", beat_emphasis)
|
||||
primary_action_custom = config.get("primary_action_custom", primary_action_custom)
|
||||
secondary_action_custom = config.get("secondary_action_custom", secondary_action_custom)
|
||||
tempo_custom = config.get("tempo_custom", tempo_custom)
|
||||
interaction_mode_custom = config.get("interaction_mode_custom", interaction_mode_custom)
|
||||
primary_notes = config.get("primary_notes", primary_notes)
|
||||
secondary_notes = config.get("secondary_notes", secondary_notes)
|
||||
tempo_notes = config.get("tempo_notes", tempo_notes)
|
||||
interaction_notes = config.get("interaction_notes", interaction_notes)
|
||||
|
||||
primary_prompt, primary_summary = _resolve_action_option(primary_action, primary_action_custom, ACTION_PRIMARY_OPTIONS)
|
||||
secondary_prompt, secondary_summary = _resolve_action_option(secondary_action, secondary_action_custom, ACTION_SECONDARY_OPTIONS)
|
||||
tempo_prompt, tempo_summary = _resolve_action_option(tempo, tempo_custom, ACTION_TEMPO_OPTIONS)
|
||||
interaction_prompt, interaction_summary = _resolve_action_option(interaction_mode, interaction_mode_custom, ACTION_INTERACTION_OPTIONS)
|
||||
|
||||
props_list = _split_tokens(props)
|
||||
beat_highlights = _split_tokens(beat_emphasis)
|
||||
risk_list = _split_tokens(risk_notes)
|
||||
|
||||
prompt_parts = [primary_prompt]
|
||||
for fragment in (secondary_prompt, tempo_prompt, interaction_prompt):
|
||||
if fragment:
|
||||
prompt_parts.append(fragment)
|
||||
if intensity_descriptor.strip():
|
||||
prompt_parts.append(intensity_descriptor.strip())
|
||||
if emotional_anchor.strip():
|
||||
prompt_parts.append(f"emotional through-line: {emotional_anchor.strip()}")
|
||||
if props_list:
|
||||
prompt_parts.append("props in play: " + ", ".join(props_list))
|
||||
action_prompt = ", ".join(part for part in prompt_parts if part)
|
||||
if environment_beat.strip():
|
||||
action_prompt += f". Environment beat: {environment_beat.strip()}"
|
||||
if dialogue_hook.strip():
|
||||
action_prompt += f". Dialogue hook: {dialogue_hook.strip()}"
|
||||
|
||||
summary_segments = []
|
||||
if primary_summary:
|
||||
summary_segments.append(f"Primary: {primary_summary}")
|
||||
if primary_notes.strip():
|
||||
summary_segments.append(primary_notes.strip())
|
||||
if secondary_summary:
|
||||
summary_segments.append(f"Secondary: {secondary_summary}")
|
||||
if secondary_notes.strip():
|
||||
summary_segments.append(secondary_notes.strip())
|
||||
if tempo_summary:
|
||||
summary_segments.append(f"Tempo: {tempo_summary}")
|
||||
if tempo_notes.strip():
|
||||
summary_segments.append(tempo_notes.strip())
|
||||
if interaction_summary:
|
||||
summary_segments.append(f"Interaction: {interaction_summary}")
|
||||
if interaction_notes.strip():
|
||||
summary_segments.append(interaction_notes.strip())
|
||||
if risk_list:
|
||||
summary_segments.append("Risks: " + ", ".join(risk_list))
|
||||
action_summary = " | ".join(segment for segment in summary_segments if segment)
|
||||
|
||||
note_lines = []
|
||||
if beat_highlights:
|
||||
note_lines.append("Beat emphasis: " + ", ".join(beat_highlights))
|
||||
if camera_language.strip():
|
||||
note_lines.append("Camera: " + camera_language.strip())
|
||||
if sound_design.strip():
|
||||
note_lines.append("Sound design: " + sound_design.strip())
|
||||
if risk_list:
|
||||
note_lines.append("Safety: " + ", ".join(risk_list))
|
||||
if props_list:
|
||||
note_lines.append("Props check: " + ", ".join(props_list))
|
||||
if environment_beat.strip():
|
||||
note_lines.append("Environment: " + environment_beat.strip())
|
||||
if dialogue_hook.strip():
|
||||
note_lines.append("Dialogue: " + dialogue_hook.strip())
|
||||
if emotional_anchor.strip():
|
||||
note_lines.append("Emotional anchor: " + emotional_anchor.strip())
|
||||
director_notes = "\n".join(f"- {line}" for line in note_lines if line)
|
||||
|
||||
return (action_prompt.strip(), action_summary, director_notes, preset_status)
|
||||
|
||||
__all__ = ["NoxPromptActionDirector"]
|
||||
@@ -0,0 +1,278 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, cast
|
||||
|
||||
from ..common import PresetMixin, _format_notes, _resolve_with_custom, _split_tokens
|
||||
from ..constants import (
|
||||
CAMERA_ADVANCED_OPTIONS,
|
||||
CAMERA_BASIC_OPTIONS,
|
||||
COLOR_TONE_OPTIONS,
|
||||
COMPOSITION_OPTIONS,
|
||||
CUSTOM_OPTION,
|
||||
LENS_ANGLE_OPTIONS,
|
||||
LENS_FOCAL_OPTIONS,
|
||||
LENS_TYPE_OPTIONS,
|
||||
MOTION_TYPE_OPTIONS,
|
||||
SHOT_SIZE_OPTIONS,
|
||||
SPECIAL_EFFECT_OPTIONS,
|
||||
TIME_OF_DAY_OPTIONS,
|
||||
VISUAL_STYLE_OPTIONS,
|
||||
)
|
||||
|
||||
|
||||
class NoxPromptAdvancedCamera(PresetMixin):
|
||||
"""Design complex camera moves, framings, and rigging notes."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
camera_basic_choices = list(CAMERA_BASIC_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
camera_adv_choices = list(CAMERA_ADVANCED_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
lens_focal_choices = list(LENS_FOCAL_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
lens_angle_choices = list(LENS_ANGLE_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
lens_type_choices = list(LENS_TYPE_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
shot_size_choices = list(SHOT_SIZE_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
composition_choices = list(COMPOSITION_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
time_of_day_choices = list(TIME_OF_DAY_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
motion_choices = list(MOTION_TYPE_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
color_tone_choices = list(COLOR_TONE_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
visual_style_choices = list(VISUAL_STYLE_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
fx_choices = list(SPECIAL_EFFECT_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"shot_size": (shot_size_choices, {"default": "Medium Shot"}),
|
||||
"lens_angle": (lens_angle_choices, {"default": "Three-Quarter"}),
|
||||
"camera_basic": (camera_basic_choices, {"default": "Dolly In"}),
|
||||
},
|
||||
"optional": {
|
||||
"lens_focal": (lens_focal_choices, {"default": "Medium"}),
|
||||
"lens_type": (lens_type_choices, {"default": "Single Shot"}),
|
||||
"composition": (composition_choices, {"default": "Balanced"}),
|
||||
"time_of_day": (time_of_day_choices, {"default": "None"}),
|
||||
"motion_type": (motion_choices, {"default": "None"}),
|
||||
"camera_advanced": (camera_adv_choices, {"default": "Steadicam Glide"}),
|
||||
"special_effect": (fx_choices, {"default": "None"}),
|
||||
"color_tone": (color_tone_choices, {"default": "None"}),
|
||||
"visual_style": (visual_style_choices, {"default": "None"}),
|
||||
"focus_subject": ("STRING", {"multiline": True, "default": ""}),
|
||||
"focus_environment": ("STRING", {"multiline": True, "default": ""}),
|
||||
"movement_beats": ("STRING", {"multiline": True, "default": ""}),
|
||||
"rig_support": ("STRING", {"multiline": True, "default": "stabilized gimbal team"}),
|
||||
"safety_considerations": ("STRING", {"multiline": True, "default": "confirm clearance paths and harness checks"}),
|
||||
"accent_keywords": ("STRING", {"multiline": True, "default": ""}),
|
||||
"shot_size_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"lens_angle_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"camera_basic_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"lens_focal_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"lens_type_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"composition_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"time_of_day_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"motion_type_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"camera_advanced_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"special_effect_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"color_tone_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"visual_style_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("camera_prompt", "framing_summary", "rigging_notes", "preset_status")
|
||||
FUNCTION = "compose"
|
||||
CATEGORY = "NoxPrompter/Camera"
|
||||
|
||||
def compose(
|
||||
self,
|
||||
shot_size,
|
||||
lens_angle,
|
||||
camera_basic,
|
||||
lens_focal="Medium",
|
||||
lens_type="Single Shot",
|
||||
composition="Balanced",
|
||||
time_of_day="None",
|
||||
motion_type="None",
|
||||
camera_advanced="Steadicam Glide",
|
||||
special_effect="None",
|
||||
color_tone="None",
|
||||
visual_style="None",
|
||||
focus_subject="",
|
||||
focus_environment="",
|
||||
movement_beats="",
|
||||
rig_support="stabilized gimbal team",
|
||||
safety_considerations="confirm clearance paths and harness checks",
|
||||
accent_keywords="",
|
||||
shot_size_custom="",
|
||||
lens_angle_custom="",
|
||||
camera_basic_custom="",
|
||||
lens_focal_custom="",
|
||||
lens_type_custom="",
|
||||
composition_custom="",
|
||||
time_of_day_custom="",
|
||||
motion_type_custom="",
|
||||
camera_advanced_custom="",
|
||||
special_effect_custom="",
|
||||
color_tone_custom="",
|
||||
visual_style_custom="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config: Dict[str, str] = {
|
||||
"shot_size": shot_size,
|
||||
"lens_angle": lens_angle,
|
||||
"camera_basic": camera_basic,
|
||||
"lens_focal": lens_focal,
|
||||
"lens_type": lens_type,
|
||||
"composition": composition,
|
||||
"time_of_day": time_of_day,
|
||||
"motion_type": motion_type,
|
||||
"camera_advanced": camera_advanced,
|
||||
"special_effect": special_effect,
|
||||
"color_tone": color_tone,
|
||||
"visual_style": visual_style,
|
||||
"focus_subject": focus_subject,
|
||||
"focus_environment": focus_environment,
|
||||
"movement_beats": movement_beats,
|
||||
"rig_support": rig_support,
|
||||
"safety_considerations": safety_considerations,
|
||||
"accent_keywords": accent_keywords,
|
||||
"shot_size_custom": shot_size_custom,
|
||||
"lens_angle_custom": lens_angle_custom,
|
||||
"camera_basic_custom": camera_basic_custom,
|
||||
"lens_focal_custom": lens_focal_custom,
|
||||
"lens_type_custom": lens_type_custom,
|
||||
"composition_custom": composition_custom,
|
||||
"time_of_day_custom": time_of_day_custom,
|
||||
"motion_type_custom": motion_type_custom,
|
||||
"camera_advanced_custom": camera_advanced_custom,
|
||||
"special_effect_custom": special_effect_custom,
|
||||
"color_tone_custom": color_tone_custom,
|
||||
"visual_style_custom": visual_style_custom,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"advanced_camera",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
shot_size = cast(str, config.get("shot_size", shot_size))
|
||||
lens_angle = cast(str, config.get("lens_angle", lens_angle))
|
||||
camera_basic = cast(str, config.get("camera_basic", camera_basic))
|
||||
lens_focal = cast(str, config.get("lens_focal", lens_focal))
|
||||
lens_type = cast(str, config.get("lens_type", lens_type))
|
||||
composition = cast(str, config.get("composition", composition))
|
||||
time_of_day = cast(str, config.get("time_of_day", time_of_day))
|
||||
motion_type = cast(str, config.get("motion_type", motion_type))
|
||||
camera_advanced = cast(str, config.get("camera_advanced", camera_advanced))
|
||||
special_effect = cast(str, config.get("special_effect", special_effect))
|
||||
color_tone = cast(str, config.get("color_tone", color_tone))
|
||||
visual_style = cast(str, config.get("visual_style", visual_style))
|
||||
focus_subject = cast(str, config.get("focus_subject", focus_subject))
|
||||
focus_environment = cast(str, config.get("focus_environment", focus_environment))
|
||||
movement_beats = cast(str, config.get("movement_beats", movement_beats))
|
||||
rig_support = cast(str, config.get("rig_support", rig_support))
|
||||
safety_considerations = cast(str, config.get("safety_considerations", safety_considerations))
|
||||
accent_keywords = cast(str, config.get("accent_keywords", accent_keywords))
|
||||
shot_size_custom = cast(str, config.get("shot_size_custom", shot_size_custom))
|
||||
lens_angle_custom = cast(str, config.get("lens_angle_custom", lens_angle_custom))
|
||||
camera_basic_custom = cast(str, config.get("camera_basic_custom", camera_basic_custom))
|
||||
lens_focal_custom = cast(str, config.get("lens_focal_custom", lens_focal_custom))
|
||||
lens_type_custom = cast(str, config.get("lens_type_custom", lens_type_custom))
|
||||
composition_custom = cast(str, config.get("composition_custom", composition_custom))
|
||||
time_of_day_custom = cast(str, config.get("time_of_day_custom", time_of_day_custom))
|
||||
motion_type_custom = cast(str, config.get("motion_type_custom", motion_type_custom))
|
||||
camera_advanced_custom = cast(str, config.get("camera_advanced_custom", camera_advanced_custom))
|
||||
special_effect_custom = cast(str, config.get("special_effect_custom", special_effect_custom))
|
||||
color_tone_custom = cast(str, config.get("color_tone_custom", color_tone_custom))
|
||||
visual_style_custom = cast(str, config.get("visual_style_custom", visual_style_custom))
|
||||
|
||||
shot_prompt, shot_notes = _resolve_with_custom(shot_size, shot_size_custom, SHOT_SIZE_OPTIONS)
|
||||
angle_prompt, angle_notes = _resolve_with_custom(lens_angle, lens_angle_custom, LENS_ANGLE_OPTIONS)
|
||||
basic_prompt, basic_notes = _resolve_with_custom(camera_basic, camera_basic_custom, CAMERA_BASIC_OPTIONS)
|
||||
focal_prompt, focal_notes = _resolve_with_custom(lens_focal, lens_focal_custom, LENS_FOCAL_OPTIONS)
|
||||
type_prompt, type_notes = _resolve_with_custom(lens_type, lens_type_custom, LENS_TYPE_OPTIONS)
|
||||
composition_prompt, composition_notes = _resolve_with_custom(composition, composition_custom, COMPOSITION_OPTIONS)
|
||||
time_prompt, time_notes = _resolve_with_custom(time_of_day, time_of_day_custom, TIME_OF_DAY_OPTIONS)
|
||||
motion_prompt, motion_notes = _resolve_with_custom(motion_type, motion_type_custom, MOTION_TYPE_OPTIONS)
|
||||
advanced_prompt, advanced_notes = _resolve_with_custom(camera_advanced, camera_advanced_custom, CAMERA_ADVANCED_OPTIONS)
|
||||
fx_prompt, fx_notes = _resolve_with_custom(special_effect, special_effect_custom, SPECIAL_EFFECT_OPTIONS)
|
||||
color_prompt, color_notes = _resolve_with_custom(color_tone, color_tone_custom, COLOR_TONE_OPTIONS)
|
||||
style_prompt, style_notes = _resolve_with_custom(visual_style, visual_style_custom, VISUAL_STYLE_OPTIONS)
|
||||
|
||||
movement_tokens = _split_tokens(movement_beats)
|
||||
accent_tokens = _split_tokens(accent_keywords)
|
||||
|
||||
camera_fragments: List[str] = [
|
||||
shot_prompt,
|
||||
angle_prompt,
|
||||
focal_prompt,
|
||||
type_prompt,
|
||||
composition_prompt,
|
||||
basic_prompt,
|
||||
advanced_prompt,
|
||||
motion_prompt,
|
||||
time_prompt,
|
||||
color_prompt,
|
||||
style_prompt,
|
||||
]
|
||||
if focus_subject.strip():
|
||||
camera_fragments.append(f"subject focus: {focus_subject.strip()}")
|
||||
if focus_environment.strip():
|
||||
camera_fragments.append(f"environment focus: {focus_environment.strip()}")
|
||||
if movement_tokens:
|
||||
camera_fragments.append("movement beats: " + ", ".join(movement_tokens))
|
||||
if accent_tokens:
|
||||
camera_fragments.append("accent keywords: " + ", ".join(accent_tokens))
|
||||
if fx_prompt:
|
||||
camera_fragments.append(fx_prompt)
|
||||
|
||||
camera_prompt = ", ".join(fragment for fragment in camera_fragments if fragment).strip()
|
||||
if camera_prompt and camera_prompt[-1] not in ".!?":
|
||||
camera_prompt += "."
|
||||
|
||||
summary_parts = [
|
||||
_format_notes("Shot", shot_notes, shot_prompt),
|
||||
_format_notes("Angle", angle_notes, angle_prompt),
|
||||
_format_notes("Lens", focal_notes, focal_prompt),
|
||||
_format_notes("Lens Type", type_notes, type_prompt),
|
||||
_format_notes("Composition", composition_notes, composition_prompt),
|
||||
_format_notes("Basic Move", basic_notes, basic_prompt),
|
||||
_format_notes("Advanced Move", advanced_notes, advanced_prompt),
|
||||
_format_notes("Motion", motion_notes, motion_prompt),
|
||||
_format_notes("Time", time_notes, time_prompt),
|
||||
_format_notes("Color", color_notes, color_prompt),
|
||||
_format_notes("Style", style_notes, style_prompt),
|
||||
_format_notes("FX", fx_notes, fx_prompt),
|
||||
]
|
||||
framing_summary = " | ".join(part for part in summary_parts if part)
|
||||
|
||||
rig_lines: List[str] = []
|
||||
if rig_support.strip():
|
||||
rig_lines.append(f"Rig Support: {rig_support.strip()}")
|
||||
if basic_notes:
|
||||
rig_lines.append(f"Basic Move Tips: {basic_notes}")
|
||||
if advanced_notes:
|
||||
rig_lines.append(f"Advanced Move Notes: {advanced_notes}")
|
||||
if movement_tokens:
|
||||
rig_lines.append("Movement coordination: " + ", ".join(movement_tokens))
|
||||
if safety_considerations.strip():
|
||||
rig_lines.append("Safety: " + safety_considerations.strip())
|
||||
if focus_subject.strip() or focus_environment.strip():
|
||||
focal_blurb = ", ".join(filter(None, [focus_subject.strip(), focus_environment.strip()]))
|
||||
rig_lines.append("Focus notes: " + focal_blurb)
|
||||
if accent_tokens:
|
||||
rig_lines.append("Accent cues: " + ", ".join(accent_tokens))
|
||||
|
||||
rigging_notes = "\n".join(f"- {line}" for line in rig_lines if line)
|
||||
|
||||
return (
|
||||
camera_prompt,
|
||||
framing_summary,
|
||||
rigging_notes,
|
||||
preset_status,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["NoxPromptAdvancedCamera"]
|
||||
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptAnalyzer:
|
||||
"""
|
||||
A node for analyzing prompt characteristics and providing feedback.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
"analysis_type": (["word_count", "complexity", "style_detection", "full_analysis"], {
|
||||
"default": "full_analysis"
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "INT", "FLOAT")
|
||||
RETURN_NAMES = ("analysis_result", "word_count", "complexity_score")
|
||||
FUNCTION = "analyze_prompt"
|
||||
CATEGORY = "NoxPrompter/Analysis"
|
||||
|
||||
def analyze_prompt(self, prompt, analysis_type):
|
||||
"""
|
||||
Analyze the prompt and return various metrics.
|
||||
"""
|
||||
|
||||
if not prompt.strip():
|
||||
return ("Empty prompt", 0, 0.0)
|
||||
|
||||
words = prompt.split()
|
||||
word_count = len(words)
|
||||
|
||||
# Calculate complexity score
|
||||
complexity_score = self._calculate_complexity(prompt)
|
||||
|
||||
if analysis_type == "word_count":
|
||||
result = f"Word count: {word_count}"
|
||||
|
||||
elif analysis_type == "complexity":
|
||||
result = f"Complexity score: {complexity_score:.2f}"
|
||||
|
||||
elif analysis_type == "style_detection":
|
||||
detected_style = self._detect_style(prompt)
|
||||
result = f"Detected style: {detected_style}"
|
||||
|
||||
else: # full_analysis
|
||||
detected_style = self._detect_style(prompt)
|
||||
sentiment = self._analyze_sentiment(prompt)
|
||||
result = f"""Prompt Analysis:
|
||||
Word Count: {word_count}
|
||||
Complexity Score: {complexity_score:.2f}
|
||||
Detected Style: {detected_style}
|
||||
Sentiment: {sentiment}
|
||||
Length: {'Short' if word_count < 10 else 'Medium' if word_count < 25 else 'Long'}"""
|
||||
|
||||
return (result, word_count, complexity_score)
|
||||
|
||||
def _calculate_complexity(self, prompt):
|
||||
"""Calculate a complexity score for the prompt."""
|
||||
words = prompt.split()
|
||||
if not words:
|
||||
return 0.0
|
||||
|
||||
# Factors that increase complexity
|
||||
avg_word_length = sum(len(word) for word in words) / len(words)
|
||||
unique_words = len(set(words))
|
||||
uniqueness_ratio = unique_words / len(words)
|
||||
|
||||
# Look for artistic/technical terms
|
||||
artistic_terms = ['masterpiece', 'detailed', 'artstation', 'concept', 'digital', 'painting']
|
||||
artistic_score = sum(1 for term in artistic_terms if term.lower() in prompt.lower())
|
||||
|
||||
complexity = (avg_word_length * 0.3 + uniqueness_ratio * 0.4 + artistic_score * 0.3)
|
||||
return min(complexity, 10.0) # Cap at 10
|
||||
|
||||
def _detect_style(self, prompt):
|
||||
"""Detect the likely style of the prompt."""
|
||||
prompt_lower = prompt.lower()
|
||||
|
||||
style_keywords = {
|
||||
'photorealistic': ['photo', 'realistic', 'photography', 'dslr', 'camera'],
|
||||
'artistic': ['painting', 'art', 'artistic', 'canvas', 'brushstroke'],
|
||||
'anime': ['anime', 'manga', 'kawaii', 'chibi'],
|
||||
'cinematic': ['cinematic', 'movie', 'film', 'dramatic'],
|
||||
'fantasy': ['fantasy', 'magical', 'mystical', 'dragon', 'wizard'],
|
||||
'sci-fi': ['sci-fi', 'futuristic', 'cyberpunk', 'robot', 'space'],
|
||||
'abstract': ['abstract', 'surreal', 'experimental']
|
||||
}
|
||||
|
||||
style_scores = {}
|
||||
for style, keywords in style_keywords.items():
|
||||
score = sum(1 for keyword in keywords if keyword in prompt_lower)
|
||||
if score > 0:
|
||||
style_scores[style] = score
|
||||
|
||||
if style_scores:
|
||||
# Find the style with the highest score
|
||||
best_style = None
|
||||
best_score = 0
|
||||
for style, score in style_scores.items():
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_style = style
|
||||
return best_style
|
||||
return "general"
|
||||
|
||||
def _analyze_sentiment(self, prompt):
|
||||
"""Basic sentiment analysis of the prompt."""
|
||||
positive_words = ['beautiful', 'amazing', 'stunning', 'gorgeous', 'magnificent', 'wonderful']
|
||||
negative_words = ['dark', 'scary', 'horror', 'grim', 'sinister', 'evil']
|
||||
|
||||
prompt_lower = prompt.lower()
|
||||
positive_count = sum(1 for word in positive_words if word in prompt_lower)
|
||||
negative_count = sum(1 for word in negative_words if word in prompt_lower)
|
||||
|
||||
if positive_count > negative_count:
|
||||
return "positive"
|
||||
elif negative_count > positive_count:
|
||||
return "negative"
|
||||
else:
|
||||
return "neutral"
|
||||
|
||||
__all__ = ["NoxPromptAnalyzer"]
|
||||
@@ -0,0 +1,444 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin, PromptFragmentFilter, _resolve_option
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptBuilder(PresetMixin):
|
||||
"""Construct prompts using formulas and curated keyword palettes."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"formula": (list(FORMULA_CONFIGS.keys()), {"default": "Advanced Formula"}),
|
||||
"subject_focus": ("STRING", {"multiline": True, "default": "Battle-hardened warrior drenched in rain, wearing leather armor and fur"}),
|
||||
"scene_setting": ("STRING", {"multiline": True, "default": "Muddy battlefield at night, lightning in the distance, flames flickering"}),
|
||||
"motion_arc": ("STRING", {"multiline": True, "default": "Charges forward roaring, swings blade in a slow-motion crash zoom"}),
|
||||
},
|
||||
"optional": {
|
||||
"narrative_hook": ("STRING", {"multiline": True, "default": "Camera locks onto her determined eyes as thunder cracks overhead"}),
|
||||
"model_emphasis": ("STRING", {"multiline": True, "default": "Cinematic aesthetic control, precise semantic adherence"}),
|
||||
"light_source": (list(LIGHT_SOURCE_OPTIONS.keys()), {"default": "Daylight"}),
|
||||
"light_quality": (list(LIGHT_QUALITY_OPTIONS.keys()), {"default": "Soft Light"}),
|
||||
"time_of_day": (list(TIME_OF_DAY_OPTIONS.keys()), {"default": "Dusk"}),
|
||||
"shot_size": (list(SHOT_SIZE_OPTIONS.keys()), {"default": "Medium Close-up"}),
|
||||
"composition": (list(COMPOSITION_OPTIONS.keys()), {"default": "Balanced"}),
|
||||
"lens_focal": (list(LENS_FOCAL_OPTIONS.keys()), {"default": "Medium"}),
|
||||
"lens_angle": (list(LENS_ANGLE_OPTIONS.keys()), {"default": "None"}),
|
||||
"lens_type": (list(LENS_TYPE_OPTIONS.keys()), {"default": "Single Shot"}),
|
||||
"color_tone": (list(COLOR_TONE_OPTIONS.keys()), {"default": "Warm Tone"}),
|
||||
"motion_type": (list(MOTION_TYPE_OPTIONS.keys()), {"default": "Running"}),
|
||||
"emotion": (list(EMOTION_OPTIONS.keys()), {"default": "Joy"}),
|
||||
"camera_basic": (list(CAMERA_BASIC_OPTIONS.keys()), {"default": "Push-in"}),
|
||||
"camera_advanced": (list(CAMERA_ADVANCED_OPTIONS.keys()), {"default": "Handheld"}),
|
||||
"visual_style": (list(VISUAL_STYLE_OPTIONS.keys()), {"default": "Anime"}),
|
||||
"special_effect": (list(SPECIAL_EFFECT_OPTIONS.keys()), {"default": "None"}),
|
||||
"keyword_style": (["auto", "inline", "compact"], {"default": "auto"}),
|
||||
"quality_profile": (["Balanced", "Cinematic", "Minimal", "Narrative"], {"default": "Balanced"}),
|
||||
"randomize_missing": ("BOOLEAN", {"default": False}),
|
||||
"random_seed": ("INT", {"default": 0, "min": 0, "max": 1_000_000, "step": 1}),
|
||||
"palette_overrides": ("STRING", {"multiline": True, "default": ""}),
|
||||
"extra_descriptors": ("STRING", {"multiline": True, "default": "Model feature emphasis: complex & dynamic motion"}),
|
||||
"custom_keywords": ("STRING", {"multiline": True, "default": ""}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"prompt_prefix": ("STRING", {"default": ""}),
|
||||
"prompt_suffix": ("STRING", {"default": ""}),
|
||||
"prompt_filter_enabled": ("BOOLEAN", {"default": True}),
|
||||
"prompt_filter_profile": (list(sorted(PromptFragmentFilter.PROFILE_PRESETS.keys())), {"default": "balanced"}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "negative_prompt", "aesthetic_notes", "dynamic_notes", "preset_status")
|
||||
FUNCTION = "build_prompt"
|
||||
CATEGORY = "NoxPrompter/Builders"
|
||||
|
||||
def build_prompt(
|
||||
self,
|
||||
formula,
|
||||
subject_focus,
|
||||
scene_setting,
|
||||
motion_arc,
|
||||
narrative_hook="",
|
||||
model_emphasis="",
|
||||
light_source="Daylight",
|
||||
light_quality="Soft Light",
|
||||
time_of_day="Dusk",
|
||||
shot_size="Medium Close-up",
|
||||
composition="Balanced",
|
||||
lens_focal="Medium",
|
||||
lens_angle="None",
|
||||
lens_type="Single Shot",
|
||||
color_tone="Warm Tone",
|
||||
motion_type="Running",
|
||||
emotion="Joy",
|
||||
camera_basic="Push-in",
|
||||
camera_advanced="Handheld",
|
||||
visual_style="Anime",
|
||||
special_effect="None",
|
||||
keyword_style="auto",
|
||||
quality_profile="Balanced",
|
||||
randomize_missing=False,
|
||||
random_seed=0,
|
||||
palette_overrides="",
|
||||
extra_descriptors="",
|
||||
custom_keywords="",
|
||||
negative_prompt="",
|
||||
prompt_prefix="",
|
||||
prompt_suffix="",
|
||||
prompt_filter_enabled=True,
|
||||
prompt_filter_profile="balanced",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"formula": formula,
|
||||
"subject_focus": subject_focus,
|
||||
"scene_setting": scene_setting,
|
||||
"motion_arc": motion_arc,
|
||||
"narrative_hook": narrative_hook,
|
||||
"model_emphasis": model_emphasis,
|
||||
"light_source": light_source,
|
||||
"light_quality": light_quality,
|
||||
"time_of_day": time_of_day,
|
||||
"shot_size": shot_size,
|
||||
"composition": composition,
|
||||
"lens_focal": lens_focal,
|
||||
"lens_angle": lens_angle,
|
||||
"lens_type": lens_type,
|
||||
"color_tone": color_tone,
|
||||
"motion_type": motion_type,
|
||||
"emotion": emotion,
|
||||
"camera_basic": camera_basic,
|
||||
"camera_advanced": camera_advanced,
|
||||
"visual_style": visual_style,
|
||||
"special_effect": special_effect,
|
||||
"keyword_style": keyword_style,
|
||||
"quality_profile": quality_profile,
|
||||
"randomize_missing": randomize_missing,
|
||||
"random_seed": random_seed,
|
||||
"palette_overrides": palette_overrides,
|
||||
"extra_descriptors": extra_descriptors,
|
||||
"custom_keywords": custom_keywords,
|
||||
"negative_prompt": negative_prompt,
|
||||
"prompt_prefix": prompt_prefix,
|
||||
"prompt_suffix": prompt_suffix,
|
||||
"prompt_filter_enabled": prompt_filter_enabled,
|
||||
"prompt_filter_profile": prompt_filter_profile,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"prompt_builder",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
formula = config["formula"]
|
||||
subject_focus = config["subject_focus"]
|
||||
scene_setting = config["scene_setting"]
|
||||
motion_arc = config["motion_arc"]
|
||||
narrative_hook = config.get("narrative_hook", "")
|
||||
model_emphasis = config.get("model_emphasis", "")
|
||||
light_source = config.get("light_source", "Daylight")
|
||||
light_quality = config.get("light_quality", "Soft Light")
|
||||
time_of_day = config.get("time_of_day", "Dusk")
|
||||
shot_size = config.get("shot_size", "Medium Close-up")
|
||||
composition = config.get("composition", "Balanced")
|
||||
lens_focal = config.get("lens_focal", "Medium")
|
||||
lens_angle = config.get("lens_angle", "None")
|
||||
lens_type = config.get("lens_type", "Single Shot")
|
||||
color_tone = config.get("color_tone", "Warm Tone")
|
||||
motion_type = config.get("motion_type", "Running")
|
||||
emotion = config.get("emotion", "Joy")
|
||||
camera_basic = config.get("camera_basic", "Push-in")
|
||||
camera_advanced = config.get("camera_advanced", "Handheld")
|
||||
visual_style = config.get("visual_style", "Anime")
|
||||
special_effect = config.get("special_effect", "None")
|
||||
keyword_style = config.get("keyword_style", "auto")
|
||||
quality_profile = config.get("quality_profile", "Balanced")
|
||||
randomize_missing = bool(config.get("randomize_missing", False))
|
||||
random_seed = int(config.get("random_seed", 0))
|
||||
palette_overrides = config.get("palette_overrides", "")
|
||||
extra_descriptors = config.get("extra_descriptors", "")
|
||||
custom_keywords = config.get("custom_keywords", "")
|
||||
negative_prompt = config.get("negative_prompt", "")
|
||||
prompt_prefix = config.get("prompt_prefix", "")
|
||||
prompt_suffix = config.get("prompt_suffix", "")
|
||||
prompt_filter_enabled = bool(config.get("prompt_filter_enabled", True))
|
||||
prompt_filter_profile = config.get("prompt_filter_profile", "balanced")
|
||||
|
||||
formula_config = FORMULA_CONFIGS.get(formula, next(iter(FORMULA_CONFIGS.values())))
|
||||
overrides_map, override_custom = self._parse_palette_overrides(palette_overrides)
|
||||
rng = random.Random(random_seed) if randomize_missing else None
|
||||
|
||||
prompt_fragments = []
|
||||
|
||||
prefix_fragment = prompt_prefix.strip()
|
||||
if prefix_fragment:
|
||||
prompt_fragments.append(prefix_fragment)
|
||||
|
||||
subject_clause = subject_focus.strip()
|
||||
if model_emphasis.strip():
|
||||
emphasis = model_emphasis.strip()
|
||||
subject_clause = f"{subject_clause}, {emphasis}" if subject_clause else emphasis
|
||||
if subject_clause:
|
||||
prompt_fragments.append(subject_clause)
|
||||
|
||||
if scene_setting.strip():
|
||||
prompt_fragments.append(scene_setting.strip())
|
||||
|
||||
if motion_arc.strip():
|
||||
prompt_fragments.append(motion_arc.strip())
|
||||
|
||||
if narrative_hook.strip():
|
||||
prompt_fragments.append(narrative_hook.strip())
|
||||
|
||||
if extra_descriptors.strip():
|
||||
prompt_fragments.append(extra_descriptors.strip())
|
||||
|
||||
aesthetic_prompts, aesthetic_summaries = self._collect_category_details(
|
||||
[
|
||||
("light_source", light_source, LIGHT_SOURCE_OPTIONS),
|
||||
("light_quality", light_quality, LIGHT_QUALITY_OPTIONS),
|
||||
("time_of_day", time_of_day, TIME_OF_DAY_OPTIONS),
|
||||
("shot_size", shot_size, SHOT_SIZE_OPTIONS),
|
||||
("composition", composition, COMPOSITION_OPTIONS),
|
||||
("lens_focal", lens_focal, LENS_FOCAL_OPTIONS),
|
||||
("lens_angle", lens_angle, LENS_ANGLE_OPTIONS),
|
||||
("lens_type", lens_type, LENS_TYPE_OPTIONS),
|
||||
("color_tone", color_tone, COLOR_TONE_OPTIONS),
|
||||
],
|
||||
overrides_map,
|
||||
rng=rng,
|
||||
randomize_missing=randomize_missing,
|
||||
)
|
||||
|
||||
dynamic_prompts, dynamic_summaries = self._collect_category_details(
|
||||
[
|
||||
("motion_type", motion_type, MOTION_TYPE_OPTIONS),
|
||||
("emotion", emotion, EMOTION_OPTIONS),
|
||||
("camera_basic", camera_basic, CAMERA_BASIC_OPTIONS),
|
||||
("camera_advanced", camera_advanced, CAMERA_ADVANCED_OPTIONS),
|
||||
],
|
||||
overrides_map,
|
||||
rng=rng,
|
||||
randomize_missing=randomize_missing,
|
||||
)
|
||||
|
||||
style_prompts, style_summaries = self._collect_category_details(
|
||||
[
|
||||
("visual_style", visual_style, VISUAL_STYLE_OPTIONS),
|
||||
("special_effect", special_effect, SPECIAL_EFFECT_OPTIONS),
|
||||
],
|
||||
overrides_map,
|
||||
rng=rng,
|
||||
randomize_missing=randomize_missing,
|
||||
)
|
||||
|
||||
keyword_mode = (keyword_style or "auto").lower()
|
||||
structure = formula_config.get("structure", [])
|
||||
|
||||
if keyword_mode == "compact":
|
||||
combined_keywords = aesthetic_prompts + dynamic_prompts + style_prompts
|
||||
if combined_keywords:
|
||||
prompt_fragments.append(f"Keywords: {', '.join(combined_keywords)}")
|
||||
elif keyword_mode == "inline":
|
||||
for chunk in aesthetic_prompts + dynamic_prompts + style_prompts:
|
||||
prompt_fragments.append(chunk)
|
||||
else:
|
||||
if "aesthetic" in structure and aesthetic_prompts:
|
||||
prompt_fragments.append(f"Cinematic palette: {', '.join(aesthetic_prompts)}")
|
||||
elif aesthetic_prompts:
|
||||
prompt_fragments.extend(aesthetic_prompts)
|
||||
|
||||
if dynamic_prompts:
|
||||
prompt_fragments.append(f"Motion & camera: {', '.join(dynamic_prompts)}")
|
||||
|
||||
if "stylization" in structure and style_prompts:
|
||||
prompt_fragments.append(f"Stylization: {', '.join(style_prompts)}")
|
||||
elif style_prompts:
|
||||
prompt_fragments.extend(style_prompts)
|
||||
|
||||
combined_custom = self._split_keywords(custom_keywords)
|
||||
if override_custom:
|
||||
combined_custom.extend(override_custom)
|
||||
if combined_custom:
|
||||
combined_custom = list(dict.fromkeys(filter(None, (item.strip() for item in combined_custom))))
|
||||
if combined_custom:
|
||||
prompt_fragments.append(", ".join(combined_custom))
|
||||
|
||||
suffix_fragment = prompt_suffix.strip()
|
||||
if suffix_fragment:
|
||||
prompt_fragments.append(suffix_fragment)
|
||||
|
||||
filter_summary = ""
|
||||
if prompt_filter_enabled:
|
||||
fragment_filter = PromptFragmentFilter(prompt_filter_profile)
|
||||
filtered_fragments = fragment_filter.organize(prompt_fragments)
|
||||
if filtered_fragments:
|
||||
prompt_fragments = filtered_fragments
|
||||
filter_summary = fragment_filter.summarize()
|
||||
if prefix_fragment:
|
||||
try:
|
||||
prompt_fragments.remove(prefix_fragment)
|
||||
except ValueError:
|
||||
pass
|
||||
prompt_fragments.insert(0, prefix_fragment)
|
||||
if suffix_fragment:
|
||||
try:
|
||||
prompt_fragments.remove(suffix_fragment)
|
||||
except ValueError:
|
||||
pass
|
||||
prompt_fragments.append(suffix_fragment)
|
||||
|
||||
prompt_text = self._assemble_prompt(prompt_fragments)
|
||||
neg_text = negative_prompt.strip()
|
||||
|
||||
aesthetic_notes = " | ".join(filter(None, aesthetic_summaries + style_summaries))
|
||||
dynamic_notes = " | ".join(filter(None, dynamic_summaries + ([filter_summary] if filter_summary else [])))
|
||||
|
||||
return (prompt_text, neg_text, aesthetic_notes, dynamic_notes, preset_status)
|
||||
|
||||
def _collect_category_details(self, selections, overrides, rng=None, randomize_missing=False):
|
||||
prompts = []
|
||||
summaries = []
|
||||
overrides = overrides or {}
|
||||
for key, label, options in selections:
|
||||
tokens = self._expand_selection(label)
|
||||
override_tokens = overrides.get(key, [])
|
||||
if override_tokens:
|
||||
tokens.extend(override_tokens)
|
||||
if not tokens:
|
||||
tokens.append(label)
|
||||
|
||||
for token in tokens:
|
||||
option = self._resolve_option(token, options, rng=rng, randomize_missing=randomize_missing)
|
||||
prompt_text = option.get("prompt", "").strip()
|
||||
summary_text = option.get("summary", "").strip()
|
||||
if prompt_text:
|
||||
prompts.append(prompt_text)
|
||||
if summary_text:
|
||||
summaries.append(summary_text)
|
||||
|
||||
prompts = list(dict.fromkeys(filter(None, prompts)))
|
||||
summaries = list(dict.fromkeys(filter(None, summaries)))
|
||||
return prompts, summaries
|
||||
|
||||
def _resolve_option(self, label, options, rng=None, randomize_missing=False):
|
||||
normalized = (label or "").strip()
|
||||
if normalized:
|
||||
if normalized.lower() == "none":
|
||||
normalized = ""
|
||||
else:
|
||||
lower_map = {name.lower(): name for name in options.keys()}
|
||||
lookup = lower_map.get(normalized.lower())
|
||||
if lookup:
|
||||
return options.get(lookup, options.get("None", {"prompt": "", "summary": ""}))
|
||||
if randomize_missing and rng:
|
||||
candidates = [name for name in options if name.lower() != "none" and options[name].get("prompt")]
|
||||
if candidates:
|
||||
choice = rng.choice(candidates)
|
||||
return options[choice]
|
||||
return options.get("None", {"prompt": "", "summary": ""})
|
||||
|
||||
def _expand_selection(self, value):
|
||||
if not value:
|
||||
return []
|
||||
tokens = [token.strip() for token in re.split(r'[|/,]', value) if token.strip()]
|
||||
return tokens
|
||||
|
||||
def _parse_palette_overrides(self, text):
|
||||
if not text or not text.strip():
|
||||
return {}, []
|
||||
|
||||
overrides = {}
|
||||
custom_chunks = []
|
||||
mapping = self._palette_alias_map()
|
||||
|
||||
for line in text.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if ":" in line:
|
||||
key_part, values_part = line.split(":", 1)
|
||||
canonical = mapping.get(self._normalize_token(key_part))
|
||||
values = self._split_keywords(values_part)
|
||||
if canonical and values:
|
||||
overrides.setdefault(canonical, []).extend(values)
|
||||
elif values:
|
||||
custom_chunks.extend(values)
|
||||
else:
|
||||
custom_chunks.extend(self._split_keywords(line))
|
||||
|
||||
for key in list(overrides.keys()):
|
||||
overrides[key] = list(dict.fromkeys(overrides[key]))
|
||||
|
||||
custom_chunks = list(dict.fromkeys(custom_chunks))
|
||||
return overrides, custom_chunks
|
||||
|
||||
def _palette_alias_map(self):
|
||||
return {
|
||||
"light source": "light_source",
|
||||
"light sources": "light_source",
|
||||
"lighting": "light_source",
|
||||
"light quality": "light_quality",
|
||||
"light qualities": "light_quality",
|
||||
"time of day": "time_of_day",
|
||||
"time": "time_of_day",
|
||||
"shot size": "shot_size",
|
||||
"composition": "composition",
|
||||
"lens focal": "lens_focal",
|
||||
"focal length": "lens_focal",
|
||||
"lens angle": "lens_angle",
|
||||
"camera angle": "lens_angle",
|
||||
"lens type": "lens_type",
|
||||
"shot type": "lens_type",
|
||||
"color tone": "color_tone",
|
||||
"color": "color_tone",
|
||||
"motion type": "motion_type",
|
||||
"motion": "motion_type",
|
||||
"emotion": "emotion",
|
||||
"camera basic": "camera_basic",
|
||||
"camera move": "camera_basic",
|
||||
"basic move": "camera_basic",
|
||||
"camera advanced": "camera_advanced",
|
||||
"advanced move": "camera_advanced",
|
||||
"visual style": "visual_style",
|
||||
"style": "visual_style",
|
||||
"special effect": "special_effect",
|
||||
"effects": "special_effect",
|
||||
}
|
||||
|
||||
def _normalize_token(self, token):
|
||||
return token.strip().lower().replace("_", " ")
|
||||
|
||||
def _split_keywords(self, text):
|
||||
if not text:
|
||||
return []
|
||||
separators = [",", "\n", ";"]
|
||||
tokens = [text]
|
||||
for sep in separators:
|
||||
temp = []
|
||||
for token in tokens:
|
||||
temp.extend(token.split(sep))
|
||||
tokens = temp
|
||||
return [token.strip() for token in tokens if token.strip()]
|
||||
|
||||
def _assemble_prompt(self, fragments):
|
||||
cleaned = []
|
||||
for fragment in fragments:
|
||||
segment = fragment.strip()
|
||||
if not segment:
|
||||
continue
|
||||
if segment[-1] not in ".!?":
|
||||
segment = f"{segment}."
|
||||
cleaned.append(segment)
|
||||
return " ".join(cleaned)
|
||||
|
||||
__all__ = ["NoxPromptBuilder"]
|
||||
@@ -0,0 +1,219 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin, _resolve_with_custom, _split_tokens
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptCamz(PresetMixin):
|
||||
"""Author cam-stream ready prompts with adult-only safeguards and production notes."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
persona_keys = list(CAMZ_PERSONA_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
stage_keys = list(CAMZ_STAGE_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
camera_keys = list(CAMZ_CAMERA_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
energy_keys = list(CAMZ_ENERGY_OPTIONS.keys()) + [CUSTOM_OPTION]
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"persona": (persona_keys, {"default": "Soft Glam Muse"}),
|
||||
"stage_theme": (stage_keys, {"default": "Velvet Boudoir"}),
|
||||
"camera_style": (camera_keys, {"default": "Intimate Close"}),
|
||||
"energy_profile": (energy_keys, {"default": "Gentle Sway"}),
|
||||
},
|
||||
"optional": {
|
||||
"wardrobe_style": ("STRING", {"multiline": True, "default": "silky lingerie set with sheer robe"}),
|
||||
"prop_list": ("STRING", {"multiline": True, "default": "vanity mirror, rose petals"}),
|
||||
"overlay_effects": ("STRING", {"multiline": True, "default": "tip goal ticker, soft sparkle particles"}),
|
||||
"chat_tone": ("STRING", {"multiline": True, "default": "sweet compliments, name drops, gratitude"}),
|
||||
"call_to_action": ("STRING", {"default": "invite viewers to join exclusive private show"}),
|
||||
"playlist_prompt": ("STRING", {"multiline": True, "default": "slow r&b, lo-fi chill"}),
|
||||
"camera_motions": ("STRING", {"multiline": True, "default": "slow zooms, minimal pans"}),
|
||||
"safety_focus": ("STRING", {"multiline": True, "default": "reinforce adult-only audience and mutual consent"}),
|
||||
"custom_persona": ("STRING", {"multiline": True, "default": ""}),
|
||||
"custom_stage": ("STRING", {"multiline": True, "default": ""}),
|
||||
"custom_camera": ("STRING", {"multiline": True, "default": ""}),
|
||||
"custom_energy": ("STRING", {"multiline": True, "default": ""}),
|
||||
"custom_negative": ("STRING", {"multiline": True, "default": ""}),
|
||||
"custom_safety_note": ("STRING", {"multiline": True, "default": ""}),
|
||||
"include_negative_baseline": ("BOOLEAN", {"default": True}),
|
||||
"include_safety_note": ("BOOLEAN", {"default": True}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("stream_prompt", "negative_prompt", "production_notes", "preset_status")
|
||||
FUNCTION = "stream"
|
||||
CATEGORY = "NoxPrompter/NSFW"
|
||||
|
||||
def stream(
|
||||
self,
|
||||
persona,
|
||||
stage_theme,
|
||||
camera_style,
|
||||
energy_profile,
|
||||
wardrobe_style="",
|
||||
prop_list="",
|
||||
overlay_effects="",
|
||||
chat_tone="",
|
||||
call_to_action="",
|
||||
playlist_prompt="",
|
||||
camera_motions="",
|
||||
safety_focus="",
|
||||
custom_persona="",
|
||||
custom_stage="",
|
||||
custom_camera="",
|
||||
custom_energy="",
|
||||
custom_negative="",
|
||||
custom_safety_note="",
|
||||
include_negative_baseline=True,
|
||||
include_safety_note=True,
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"persona": persona,
|
||||
"stage_theme": stage_theme,
|
||||
"camera_style": camera_style,
|
||||
"energy_profile": energy_profile,
|
||||
"wardrobe_style": wardrobe_style,
|
||||
"prop_list": prop_list,
|
||||
"overlay_effects": overlay_effects,
|
||||
"chat_tone": chat_tone,
|
||||
"call_to_action": call_to_action,
|
||||
"playlist_prompt": playlist_prompt,
|
||||
"camera_motions": camera_motions,
|
||||
"safety_focus": safety_focus,
|
||||
"custom_persona": custom_persona,
|
||||
"custom_stage": custom_stage,
|
||||
"custom_camera": custom_camera,
|
||||
"custom_energy": custom_energy,
|
||||
"custom_negative": custom_negative,
|
||||
"custom_safety_note": custom_safety_note,
|
||||
"include_negative_baseline": include_negative_baseline,
|
||||
"include_safety_note": include_safety_note,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"nox_camz",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
persona = config.get("persona", persona)
|
||||
stage_theme = config.get("stage_theme", stage_theme)
|
||||
camera_style = config.get("camera_style", camera_style)
|
||||
energy_profile = config.get("energy_profile", energy_profile)
|
||||
wardrobe_style = config.get("wardrobe_style", wardrobe_style)
|
||||
prop_list = config.get("prop_list", prop_list)
|
||||
overlay_effects = config.get("overlay_effects", overlay_effects)
|
||||
chat_tone = config.get("chat_tone", chat_tone)
|
||||
call_to_action = config.get("call_to_action", call_to_action)
|
||||
playlist_prompt = config.get("playlist_prompt", playlist_prompt)
|
||||
camera_motions = config.get("camera_motions", camera_motions)
|
||||
safety_focus = config.get("safety_focus", safety_focus)
|
||||
custom_persona = config.get("custom_persona", custom_persona)
|
||||
custom_stage = config.get("custom_stage", custom_stage)
|
||||
custom_camera = config.get("custom_camera", custom_camera)
|
||||
custom_energy = config.get("custom_energy", custom_energy)
|
||||
custom_negative = config.get("custom_negative", custom_negative)
|
||||
custom_safety_note = config.get("custom_safety_note", custom_safety_note)
|
||||
include_negative_baseline = bool(config.get("include_negative_baseline", include_negative_baseline))
|
||||
include_safety_note = bool(config.get("include_safety_note", include_safety_note))
|
||||
|
||||
persona_prompt, persona_note = _resolve_with_custom(persona, custom_persona, CAMZ_PERSONA_OPTIONS)
|
||||
stage_prompt, stage_note = _resolve_with_custom(stage_theme, custom_stage, CAMZ_STAGE_OPTIONS)
|
||||
camera_prompt, camera_note = _resolve_with_custom(camera_style, custom_camera, CAMZ_CAMERA_OPTIONS)
|
||||
energy_prompt, energy_note = _resolve_with_custom(energy_profile, custom_energy, CAMZ_ENERGY_OPTIONS)
|
||||
|
||||
wardrobe_tokens = _split_tokens(wardrobe_style)
|
||||
prop_tokens = _split_tokens(prop_list)
|
||||
overlay_tokens = _split_tokens(overlay_effects)
|
||||
chat_tokens = _split_tokens(chat_tone)
|
||||
playlist_tokens = _split_tokens(playlist_prompt)
|
||||
motion_tokens = _split_tokens(camera_motions)
|
||||
|
||||
positive_segments = [
|
||||
"4k streaming cam capture, 60fps fluidity, direct lens eye contact, cinematic webcam depth-of-field",
|
||||
persona_prompt,
|
||||
stage_prompt,
|
||||
camera_prompt,
|
||||
energy_prompt,
|
||||
]
|
||||
|
||||
if wardrobe_tokens:
|
||||
positive_segments.append("wardrobe focus: " + ", ".join(wardrobe_tokens))
|
||||
if prop_tokens:
|
||||
positive_segments.append("set dressing: " + ", ".join(prop_tokens))
|
||||
if overlay_tokens:
|
||||
positive_segments.append("live overlays: " + ", ".join(overlay_tokens))
|
||||
if chat_tokens:
|
||||
positive_segments.append("viewer interaction tone: " + ", ".join(chat_tokens))
|
||||
if playlist_tokens:
|
||||
positive_segments.append("audio bed: " + ", ".join(playlist_tokens))
|
||||
if motion_tokens:
|
||||
positive_segments.append("camera motions: " + ", ".join(motion_tokens))
|
||||
if call_to_action.strip():
|
||||
positive_segments.append(call_to_action.strip())
|
||||
|
||||
stream_prompt = ", ".join(segment for segment in positive_segments if segment)
|
||||
|
||||
negative_terms = []
|
||||
if include_negative_baseline:
|
||||
negative_terms.extend(NSFW_BASELINE_NEGATIVES)
|
||||
negative_terms.extend(_split_tokens(custom_negative))
|
||||
dedup_terms: List[str] = []
|
||||
seen_terms = set()
|
||||
for term in negative_terms:
|
||||
normalized = term.strip()
|
||||
if not normalized:
|
||||
continue
|
||||
key = normalized.lower()
|
||||
if key not in seen_terms:
|
||||
seen_terms.add(key)
|
||||
dedup_terms.append(normalized)
|
||||
if not dedup_terms:
|
||||
dedup_terms = list(NSFW_BASELINE_NEGATIVES)
|
||||
negative_prompt = ", ".join(dedup_terms)
|
||||
|
||||
production_lines = []
|
||||
if persona_note:
|
||||
production_lines.append(f"Persona: {persona_note}")
|
||||
if stage_note:
|
||||
production_lines.append(f"Stage: {stage_note}")
|
||||
if camera_note:
|
||||
production_lines.append(f"Camera: {camera_note}")
|
||||
if energy_note:
|
||||
production_lines.append(f"Energy: {energy_note}")
|
||||
if overlay_tokens:
|
||||
production_lines.append("Overlay cues: " + ", ".join(overlay_tokens))
|
||||
if playlist_tokens:
|
||||
production_lines.append("Playlist: " + ", ".join(playlist_tokens))
|
||||
if motion_tokens:
|
||||
production_lines.append("Camera moves: " + ", ".join(motion_tokens))
|
||||
if safety_focus.strip():
|
||||
production_lines.append("Safety focus: " + safety_focus.strip())
|
||||
if call_to_action.strip():
|
||||
production_lines.append("CTA: " + call_to_action.strip())
|
||||
|
||||
safety_lines = []
|
||||
if include_safety_note:
|
||||
safety_lines.append("For consenting adult audiences only. Verify age compliance and platform rules before streaming.")
|
||||
if custom_safety_note.strip():
|
||||
safety_lines.append(custom_safety_note.strip())
|
||||
if safety_lines:
|
||||
production_lines.append("Safety: " + " | ".join(safety_lines))
|
||||
|
||||
production_notes = "\n".join(production_lines)
|
||||
|
||||
return (
|
||||
stream_prompt.strip(),
|
||||
negative_prompt,
|
||||
production_notes,
|
||||
preset_status,
|
||||
)
|
||||
|
||||
__all__ = ["NoxPromptCamz"]
|
||||
@@ -0,0 +1,238 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin, _resolve_with_custom, _split_tokens
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptCharacterCreator(PresetMixin):
|
||||
"""Build richly-detailed character prompts and quick-reference sheets."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"species": (list(CHARACTER_SPECIES_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Human"}),
|
||||
"archetype": (list(CHARACTER_ARCHETYPE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Champion"}),
|
||||
"profession": (list(CHARACTER_PROFESSION_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Starship Captain"}),
|
||||
},
|
||||
"optional": {
|
||||
"temperament": (list(CHARACTER_TEMPERAMENT_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Stoic"}),
|
||||
"power_source": (list(CHARACTER_POWER_SOURCE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Arcane Relic"}),
|
||||
"signature_gear": (list(CHARACTER_SIGNATURE_GEAR_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Living Blade"}),
|
||||
"visual_motif": (list(CHARACTER_VISUAL_MOTIF_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Solar Flare"}),
|
||||
"era": (list(CHARACTER_ERA_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Mythic Age"}),
|
||||
"body_type": (list(CHARACTER_BODY_TYPE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Athletic"}),
|
||||
"alignment": (list(CHARACTER_ALIGNMENT_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Lawful Good"}),
|
||||
"team_role": (list(CHARACTER_ROLE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Party Leader"}),
|
||||
"catchphrase": ("STRING", {"default": ""}),
|
||||
"signature_move": ("STRING", {"default": ""}),
|
||||
"backstory_snippet": ("STRING", {"multiline": True, "default": ""}),
|
||||
"custom_traits": ("STRING", {"multiline": True, "default": ""}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"presence_rating": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"species_custom": ("STRING", {"default": ""}),
|
||||
"archetype_custom": ("STRING", {"default": ""}),
|
||||
"profession_custom": ("STRING", {"default": ""}),
|
||||
"temperament_custom": ("STRING", {"default": ""}),
|
||||
"power_source_custom": ("STRING", {"default": ""}),
|
||||
"signature_gear_custom": ("STRING", {"default": ""}),
|
||||
"visual_motif_custom": ("STRING", {"default": ""}),
|
||||
"era_custom": ("STRING", {"default": ""}),
|
||||
"body_type_custom": ("STRING", {"default": ""}),
|
||||
"alignment_custom": ("STRING", {"default": ""}),
|
||||
"team_role_custom": ("STRING", {"default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("character_prompt", "character_sheet", "story_hooks", "preset_status")
|
||||
FUNCTION = "craft"
|
||||
CATEGORY = "NoxPrompter/Character"
|
||||
|
||||
def craft(
|
||||
self,
|
||||
species,
|
||||
archetype,
|
||||
profession,
|
||||
temperament="Stoic",
|
||||
power_source="Arcane Relic",
|
||||
signature_gear="Living Blade",
|
||||
visual_motif="Solar Flare",
|
||||
era="Mythic Age",
|
||||
body_type="Athletic",
|
||||
alignment="Lawful Good",
|
||||
team_role="Party Leader",
|
||||
species_custom="",
|
||||
archetype_custom="",
|
||||
profession_custom="",
|
||||
temperament_custom="",
|
||||
power_source_custom="",
|
||||
signature_gear_custom="",
|
||||
visual_motif_custom="",
|
||||
era_custom="",
|
||||
body_type_custom="",
|
||||
alignment_custom="",
|
||||
team_role_custom="",
|
||||
catchphrase="",
|
||||
signature_move="",
|
||||
backstory_snippet="",
|
||||
custom_traits="",
|
||||
negative_prompt="",
|
||||
presence_rating=0.75,
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"species": species,
|
||||
"archetype": archetype,
|
||||
"profession": profession,
|
||||
"temperament": temperament,
|
||||
"power_source": power_source,
|
||||
"signature_gear": signature_gear,
|
||||
"visual_motif": visual_motif,
|
||||
"era": era,
|
||||
"body_type": body_type,
|
||||
"alignment": alignment,
|
||||
"team_role": team_role,
|
||||
"species_custom": species_custom,
|
||||
"archetype_custom": archetype_custom,
|
||||
"profession_custom": profession_custom,
|
||||
"temperament_custom": temperament_custom,
|
||||
"power_source_custom": power_source_custom,
|
||||
"signature_gear_custom": signature_gear_custom,
|
||||
"visual_motif_custom": visual_motif_custom,
|
||||
"era_custom": era_custom,
|
||||
"body_type_custom": body_type_custom,
|
||||
"alignment_custom": alignment_custom,
|
||||
"team_role_custom": team_role_custom,
|
||||
"catchphrase": catchphrase,
|
||||
"signature_move": signature_move,
|
||||
"backstory_snippet": backstory_snippet,
|
||||
"custom_traits": custom_traits,
|
||||
"negative_prompt": negative_prompt,
|
||||
"presence_rating": presence_rating,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"character_creator",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
species = config.get("species", species)
|
||||
archetype = config.get("archetype", archetype)
|
||||
profession = config.get("profession", profession)
|
||||
temperament = config.get("temperament", temperament)
|
||||
power_source = config.get("power_source", power_source)
|
||||
signature_gear = config.get("signature_gear", signature_gear)
|
||||
visual_motif = config.get("visual_motif", visual_motif)
|
||||
era = config.get("era", era)
|
||||
body_type = config.get("body_type", body_type)
|
||||
alignment = config.get("alignment", alignment)
|
||||
team_role = config.get("team_role", team_role)
|
||||
species_custom = config.get("species_custom", species_custom)
|
||||
archetype_custom = config.get("archetype_custom", archetype_custom)
|
||||
profession_custom = config.get("profession_custom", profession_custom)
|
||||
temperament_custom = config.get("temperament_custom", temperament_custom)
|
||||
power_source_custom = config.get("power_source_custom", power_source_custom)
|
||||
signature_gear_custom = config.get("signature_gear_custom", signature_gear_custom)
|
||||
visual_motif_custom = config.get("visual_motif_custom", visual_motif_custom)
|
||||
era_custom = config.get("era_custom", era_custom)
|
||||
body_type_custom = config.get("body_type_custom", body_type_custom)
|
||||
alignment_custom = config.get("alignment_custom", alignment_custom)
|
||||
team_role_custom = config.get("team_role_custom", team_role_custom)
|
||||
catchphrase = config.get("catchphrase", catchphrase)
|
||||
signature_move = config.get("signature_move", signature_move)
|
||||
backstory_snippet = config.get("backstory_snippet", backstory_snippet)
|
||||
custom_traits = config.get("custom_traits", custom_traits)
|
||||
negative_prompt = config.get("negative_prompt", negative_prompt)
|
||||
presence_rating = float(config.get("presence_rating", presence_rating))
|
||||
species_prompt, species_notes = _resolve_with_custom(species, species_custom, CHARACTER_SPECIES_OPTIONS)
|
||||
archetype_prompt, archetype_notes = _resolve_with_custom(archetype, archetype_custom, CHARACTER_ARCHETYPE_OPTIONS)
|
||||
profession_prompt, profession_notes = _resolve_with_custom(profession, profession_custom, CHARACTER_PROFESSION_OPTIONS)
|
||||
temperament_prompt, temperament_notes = _resolve_with_custom(temperament, temperament_custom, CHARACTER_TEMPERAMENT_OPTIONS)
|
||||
power_prompt, power_notes = _resolve_with_custom(power_source, power_source_custom, CHARACTER_POWER_SOURCE_OPTIONS)
|
||||
gear_prompt, gear_notes = _resolve_with_custom(signature_gear, signature_gear_custom, CHARACTER_SIGNATURE_GEAR_OPTIONS)
|
||||
motif_prompt, motif_notes = _resolve_with_custom(visual_motif, visual_motif_custom, CHARACTER_VISUAL_MOTIF_OPTIONS)
|
||||
era_prompt, era_notes = _resolve_with_custom(era, era_custom, CHARACTER_ERA_OPTIONS)
|
||||
body_prompt, body_notes = _resolve_with_custom(body_type, body_type_custom, CHARACTER_BODY_TYPE_OPTIONS)
|
||||
alignment_prompt, alignment_notes = _resolve_with_custom(alignment, alignment_custom, CHARACTER_ALIGNMENT_OPTIONS)
|
||||
role_prompt, role_notes = _resolve_with_custom(team_role, team_role_custom, CHARACTER_ROLE_OPTIONS)
|
||||
|
||||
traits = _split_tokens(custom_traits)
|
||||
trait_prompt = ", ".join(traits)
|
||||
|
||||
presence_descriptor = self._describe_presence(presence_rating)
|
||||
|
||||
prompt_parts = [
|
||||
species_prompt,
|
||||
body_prompt,
|
||||
archetype_prompt,
|
||||
profession_prompt,
|
||||
temperament_prompt,
|
||||
power_prompt,
|
||||
motif_prompt,
|
||||
gear_prompt,
|
||||
alignment_prompt,
|
||||
role_prompt,
|
||||
f"signature move: {signature_move.strip()}" if signature_move.strip() else "",
|
||||
f"operates during {era_prompt}" if era_prompt else "",
|
||||
trait_prompt,
|
||||
f"presence reads as {presence_descriptor}",
|
||||
]
|
||||
|
||||
character_prompt = ", ".join(part for part in prompt_parts if part).strip()
|
||||
if backstory_snippet.strip():
|
||||
character_prompt = f"{character_prompt}. {backstory_snippet.strip()}" if character_prompt else backstory_snippet.strip()
|
||||
if character_prompt and character_prompt[-1] not in ".!?":
|
||||
character_prompt += "."
|
||||
|
||||
sheet_lines = [
|
||||
f"Species Notes: {species_notes}" if species_notes else "",
|
||||
f"Archetype Notes: {archetype_notes}" if archetype_notes else "",
|
||||
f"Profession Notes: {profession_notes}" if profession_notes else "",
|
||||
f"Temperament: {temperament_notes}" if temperament_notes else "",
|
||||
f"Power Source: {power_notes}" if power_notes else "",
|
||||
f"Signature Gear: {gear_notes}" if gear_notes else "",
|
||||
f"Visual Motif: {motif_notes}" if motif_notes else "",
|
||||
f"Era Context: {era_notes}" if era_notes else "",
|
||||
f"Body Type: {body_notes}" if body_notes else "",
|
||||
f"Alignment: {alignment_notes}" if alignment_notes else "",
|
||||
f"Team Role: {role_notes}" if role_notes else "",
|
||||
]
|
||||
if traits:
|
||||
sheet_lines.append("Distinct Traits: " + ", ".join(traits))
|
||||
|
||||
character_sheet = " | ".join(line for line in sheet_lines if line)
|
||||
|
||||
hooks = [
|
||||
f"Catchphrase: \"{catchphrase.strip()}\"" if catchphrase.strip() else "",
|
||||
f"Signature Move: {signature_move.strip()}" if signature_move.strip() else "",
|
||||
f"Team Role: {team_role}" if team_role else "",
|
||||
f"Alignment: {alignment}" if alignment else "",
|
||||
f"Presence Rating: {presence_rating:.2f} ({presence_descriptor})",
|
||||
]
|
||||
if negative_prompt.strip():
|
||||
hooks.append("Avoid: " + negative_prompt.strip())
|
||||
story_hooks = " | ".join(item for item in hooks if item)
|
||||
|
||||
return (character_prompt, character_sheet, story_hooks, preset_status)
|
||||
|
||||
def _describe_presence(self, value: float) -> str:
|
||||
clamped = max(0.0, min(1.0, value))
|
||||
if clamped < 0.15:
|
||||
return "whisper-soft aura"
|
||||
if clamped < 0.35:
|
||||
return "subtle undercurrent"
|
||||
if clamped < 0.55:
|
||||
return "balanced presence"
|
||||
if clamped < 0.75:
|
||||
return "commanding focus"
|
||||
if clamped < 0.9:
|
||||
return "formidable charisma"
|
||||
return "legendary gravitas"
|
||||
|
||||
__all__ = ["NoxPromptCharacterCreator"]
|
||||
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptCombiner:
|
||||
"""
|
||||
A node for combining multiple prompts with different blending modes.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt_1": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
"prompt_2": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
"combination_mode": (["concatenate", "blend", "alternate", "weighted"], {
|
||||
"default": "concatenate"
|
||||
}),
|
||||
"weight_1": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"weight_2": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
},
|
||||
"optional": {
|
||||
"prompt_3": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
"separator": ("STRING", {
|
||||
"default": ", "
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("combined_prompt",)
|
||||
FUNCTION = "combine_prompts"
|
||||
CATEGORY = "NoxPrompter/Text"
|
||||
|
||||
def combine_prompts(self, prompt_1, prompt_2, combination_mode, weight_1, weight_2,
|
||||
prompt_3="", separator=", "):
|
||||
"""
|
||||
Combine prompts using different combination modes.
|
||||
"""
|
||||
|
||||
prompts = [p.strip() for p in [prompt_1, prompt_2, prompt_3] if p.strip()]
|
||||
|
||||
if not prompts:
|
||||
return ("",)
|
||||
|
||||
if len(prompts) == 1:
|
||||
return (prompts[0],)
|
||||
|
||||
if combination_mode == "concatenate":
|
||||
return (separator.join(prompts),)
|
||||
|
||||
elif combination_mode == "blend":
|
||||
# Simple blending - interleave words
|
||||
words = []
|
||||
all_words = [p.split() for p in prompts]
|
||||
max_len = max(len(w) for w in all_words)
|
||||
|
||||
for i in range(max_len):
|
||||
for word_list in all_words:
|
||||
if i < len(word_list):
|
||||
words.append(word_list[i])
|
||||
|
||||
return (" ".join(words),)
|
||||
|
||||
elif combination_mode == "alternate":
|
||||
# Alternate between prompts sentence by sentence
|
||||
sentences = []
|
||||
for i, prompt in enumerate(prompts):
|
||||
prompt_sentences = [s.strip() for s in prompt.split('.') if s.strip()]
|
||||
for j, sentence in enumerate(prompt_sentences):
|
||||
if (i + j) % len(prompts) == 0:
|
||||
sentences.append(sentence)
|
||||
|
||||
return (". ".join(sentences) + ".",)
|
||||
|
||||
elif combination_mode == "weighted":
|
||||
# Apply weights using parentheses
|
||||
weighted_prompts = []
|
||||
weights = [weight_1, weight_2, 1.0] # Default weight for prompt_3
|
||||
|
||||
for prompt, weight in zip(prompts, weights[:len(prompts)]):
|
||||
if weight > 1.0:
|
||||
emphasis_level = int((weight - 1.0) * 10)
|
||||
emphasis_chars = "(" * min(emphasis_level, 5)
|
||||
closing_chars = ")" * len(emphasis_chars)
|
||||
weighted_prompts.append(f"{emphasis_chars}{prompt}{closing_chars}")
|
||||
elif weight < 1.0:
|
||||
# Use square brackets for de-emphasis
|
||||
deemphasis_level = int((1.0 - weight) * 10)
|
||||
deemphasis_chars = "[" * min(deemphasis_level, 5)
|
||||
closing_chars = "]" * len(deemphasis_chars)
|
||||
weighted_prompts.append(f"{deemphasis_chars}{prompt}{closing_chars}")
|
||||
else:
|
||||
weighted_prompts.append(prompt)
|
||||
|
||||
return (separator.join(weighted_prompts),)
|
||||
|
||||
return (separator.join(prompts),)
|
||||
|
||||
__all__ = ["NoxPromptCombiner"]
|
||||
@@ -0,0 +1,190 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptEnhancer(PresetMixin):
|
||||
"""
|
||||
A custom ComfyUI node for enhancing and manipulating text prompts.
|
||||
Provides various prompt enhancement features including style application,
|
||||
keyword emphasis, and prompt combination.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"base_prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": "a beautiful landscape"
|
||||
}),
|
||||
"enhancement_mode": (["none", "artistic", "photorealistic", "cinematic", "fantasy", "sci-fi", "portrait"], {
|
||||
"default": "none"
|
||||
}),
|
||||
"emphasis_strength": ("FLOAT", {
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.1
|
||||
}),
|
||||
"add_quality_tags": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"secondary_prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
"negative_prompt": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
"custom_style": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": ""
|
||||
}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("enhanced_prompt", "negative_prompt", "preset_status")
|
||||
FUNCTION = "enhance_prompt"
|
||||
CATEGORY = "NoxPrompter/Text"
|
||||
|
||||
def enhance_prompt(self, base_prompt, enhancement_mode, emphasis_strength, add_quality_tags,
|
||||
secondary_prompt="", negative_prompt="", custom_style="",
|
||||
preset_action="none", preset_name=""):
|
||||
"""
|
||||
Enhance the input prompt based on the selected parameters.
|
||||
"""
|
||||
|
||||
config = {
|
||||
"base_prompt": base_prompt,
|
||||
"enhancement_mode": enhancement_mode,
|
||||
"emphasis_strength": emphasis_strength,
|
||||
"add_quality_tags": add_quality_tags,
|
||||
"secondary_prompt": secondary_prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"custom_style": custom_style,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"prompt_enhancer",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
base_prompt = config["base_prompt"]
|
||||
enhancement_mode = config["enhancement_mode"]
|
||||
emphasis_strength = float(config["emphasis_strength"])
|
||||
add_quality_tags = bool(config["add_quality_tags"])
|
||||
secondary_prompt = config.get("secondary_prompt", "")
|
||||
negative_prompt = config.get("negative_prompt", "")
|
||||
custom_style = config.get("custom_style", "")
|
||||
|
||||
# Start with the base prompt
|
||||
enhanced_prompt = base_prompt.strip()
|
||||
|
||||
# Apply emphasis strength if greater than 1.0
|
||||
if emphasis_strength > 1.0:
|
||||
# Add parentheses for emphasis (ComfyUI/Stable Diffusion syntax)
|
||||
emphasis_level = int((emphasis_strength - 1.0) * 10)
|
||||
emphasis_chars = "(" * min(emphasis_level, 5) # Cap at 5 levels
|
||||
closing_chars = ")" * len(emphasis_chars)
|
||||
enhanced_prompt = f"{emphasis_chars}{enhanced_prompt}{closing_chars}"
|
||||
|
||||
# Apply enhancement mode styles
|
||||
style_additions = self._get_style_additions(enhancement_mode)
|
||||
if style_additions:
|
||||
enhanced_prompt = f"{enhanced_prompt}, {style_additions}"
|
||||
|
||||
# Add custom style if provided
|
||||
if custom_style.strip():
|
||||
enhanced_prompt = f"{enhanced_prompt}, {custom_style.strip()}"
|
||||
|
||||
# Combine with secondary prompt if provided
|
||||
if secondary_prompt.strip():
|
||||
enhanced_prompt = f"{enhanced_prompt}, {secondary_prompt.strip()}"
|
||||
|
||||
# Add quality tags if enabled
|
||||
if add_quality_tags:
|
||||
quality_tags = self._get_quality_tags(enhancement_mode)
|
||||
enhanced_prompt = f"{enhanced_prompt}, {quality_tags}"
|
||||
|
||||
# Process negative prompt
|
||||
processed_negative = self._process_negative_prompt(negative_prompt, enhancement_mode)
|
||||
|
||||
# Clean up the final prompt
|
||||
enhanced_prompt = self._clean_prompt(enhanced_prompt)
|
||||
|
||||
return (enhanced_prompt, processed_negative, preset_status)
|
||||
|
||||
def _get_style_additions(self, mode):
|
||||
"""Get style-specific prompt additions based on enhancement mode."""
|
||||
styles = {
|
||||
"artistic": "artistic, painterly, expressive brushstrokes, vibrant colors",
|
||||
"photorealistic": "photorealistic, highly detailed, sharp focus, professional photography",
|
||||
"cinematic": "cinematic lighting, dramatic composition, film grain, movie still",
|
||||
"fantasy": "fantasy art, magical atmosphere, ethereal lighting, mystical",
|
||||
"sci-fi": "sci-fi, futuristic, high-tech, neon lighting, cyberpunk aesthetic",
|
||||
"portrait": "portrait photography, professional lighting, shallow depth of field, bokeh"
|
||||
}
|
||||
return styles.get(mode, "")
|
||||
|
||||
def _get_quality_tags(self, mode):
|
||||
"""Get quality enhancement tags based on the mode."""
|
||||
base_quality = "high quality, detailed, masterpiece"
|
||||
|
||||
mode_specific = {
|
||||
"artistic": "8k resolution, trending on artstation",
|
||||
"photorealistic": "8k uhd, dslr, soft lighting, high quality",
|
||||
"cinematic": "4k, imax, film photography",
|
||||
"fantasy": "concept art, digital painting, trending on artstation",
|
||||
"sci-fi": "concept art, digital art, trending on artstation",
|
||||
"portrait": "professional photography, studio lighting"
|
||||
}
|
||||
|
||||
specific_tags = mode_specific.get(mode, "best quality")
|
||||
return f"{base_quality}, {specific_tags}"
|
||||
|
||||
def _process_negative_prompt(self, negative_prompt, mode):
|
||||
"""Process and enhance the negative prompt based on mode."""
|
||||
base_negative = negative_prompt.strip() if negative_prompt.strip() else ""
|
||||
|
||||
# Add common negative prompts based on mode
|
||||
mode_negatives = {
|
||||
"photorealistic": "cartoon, anime, painting, drawing, illustration",
|
||||
"artistic": "photograph, photo, realistic",
|
||||
"cinematic": "amateur, low quality, blurry",
|
||||
"fantasy": "modern, contemporary, realistic photography",
|
||||
"sci-fi": "medieval, ancient, primitive",
|
||||
"portrait": "full body, landscape, multiple people"
|
||||
}
|
||||
|
||||
mode_specific = mode_negatives.get(mode, "")
|
||||
|
||||
# Combine negatives
|
||||
if base_negative and mode_specific:
|
||||
return f"{base_negative}, {mode_specific}"
|
||||
elif mode_specific:
|
||||
return mode_specific
|
||||
else:
|
||||
return base_negative
|
||||
|
||||
def _clean_prompt(self, prompt):
|
||||
"""Clean up the prompt by removing duplicate commas and extra spaces."""
|
||||
# Remove multiple consecutive commas
|
||||
prompt = re.sub(r',\s*,', ',', prompt)
|
||||
# Remove spaces before commas
|
||||
prompt = re.sub(r'\s+,', ',', prompt)
|
||||
# Remove multiple consecutive spaces
|
||||
prompt = re.sub(r'\s+', ' ', prompt)
|
||||
# Add space after commas if missing
|
||||
prompt = re.sub(r',([^\s])', r', \1', prompt)
|
||||
return prompt.strip()
|
||||
|
||||
__all__ = ["NoxPromptEnhancer"]
|
||||
@@ -0,0 +1,299 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..common import PresetMixin, _resolve_with_custom, _split_tokens
|
||||
from ..constants import (
|
||||
CUSTOM_OPTION,
|
||||
HUMAN_DESIGN_BEAUTY_MARK_OPTIONS,
|
||||
HUMAN_DESIGN_BODY_SHAPE_OPTIONS,
|
||||
HUMAN_DESIGN_BODY_TYPE_OPTIONS,
|
||||
HUMAN_DESIGN_EYE_COLOR_OPTIONS,
|
||||
HUMAN_DESIGN_EYE_DETAIL_OPTIONS,
|
||||
HUMAN_DESIGN_FACE_OPTIONS,
|
||||
HUMAN_DESIGN_GENDER_OPTIONS,
|
||||
HUMAN_DESIGN_GENITAL_OPTIONS,
|
||||
HUMAN_DESIGN_HAIR_COLOR_OPTIONS,
|
||||
HUMAN_DESIGN_HAIR_LENGTH_OPTIONS,
|
||||
HUMAN_DESIGN_HAIR_STYLE_OPTIONS,
|
||||
HUMAN_DESIGN_HEIGHT_OPTIONS,
|
||||
HUMAN_DESIGN_SKIN_DETAIL_OPTIONS,
|
||||
HUMAN_DESIGN_SKIN_TONE_OPTIONS,
|
||||
)
|
||||
|
||||
class NoxPromptHumanDesigner(PresetMixin):
|
||||
"""Curate adult human anatomy and styling breakdowns."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"gender": (list(HUMAN_DESIGN_GENDER_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Adult Woman"}),
|
||||
"height": (list(HUMAN_DESIGN_HEIGHT_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Average"}),
|
||||
"face_type": (list(HUMAN_DESIGN_FACE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Oval"}),
|
||||
"body_type": (list(HUMAN_DESIGN_BODY_TYPE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Athletic"}),
|
||||
"body_shape": (list(HUMAN_DESIGN_BODY_SHAPE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Hourglass"}),
|
||||
},
|
||||
"optional": {
|
||||
"hair_length": (list(HUMAN_DESIGN_HAIR_LENGTH_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Shoulder"}),
|
||||
"hair_color": (list(HUMAN_DESIGN_HAIR_COLOR_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Deep Brunette"}),
|
||||
"hair_style": (list(HUMAN_DESIGN_HAIR_STYLE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Loose Waves"}),
|
||||
"eye_detail": (list(HUMAN_DESIGN_EYE_DETAIL_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Almond Focus"}),
|
||||
"eye_color": (list(HUMAN_DESIGN_EYE_COLOR_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Amber Luminescence"}),
|
||||
"skin_tone": (list(HUMAN_DESIGN_SKIN_TONE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Warm Honey"}),
|
||||
"skin_detail": (list(HUMAN_DESIGN_SKIN_DETAIL_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Soft Glow"}),
|
||||
"beauty_marks": (list(HUMAN_DESIGN_BEAUTY_MARK_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Bare"}),
|
||||
"genital_description": (list(HUMAN_DESIGN_GENITAL_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Discrete"}),
|
||||
"accent_features": ("STRING", {"multiline": True, "default": ""}),
|
||||
"pose_hint": ("STRING", {"default": ""}),
|
||||
"consent_language": ("STRING", {"default": "All subjects depicted are consenting adults aged 18+."}),
|
||||
"safety_addendum": ("STRING", {"default": ""}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"gender_custom": ("STRING", {"default": ""}),
|
||||
"height_custom": ("STRING", {"default": ""}),
|
||||
"face_type_custom": ("STRING", {"default": ""}),
|
||||
"body_type_custom": ("STRING", {"default": ""}),
|
||||
"body_shape_custom": ("STRING", {"default": ""}),
|
||||
"hair_length_custom": ("STRING", {"default": ""}),
|
||||
"hair_color_custom": ("STRING", {"default": ""}),
|
||||
"hair_style_custom": ("STRING", {"default": ""}),
|
||||
"eye_detail_custom": ("STRING", {"default": ""}),
|
||||
"eye_color_custom": ("STRING", {"default": ""}),
|
||||
"skin_tone_custom": ("STRING", {"default": ""}),
|
||||
"skin_detail_custom": ("STRING", {"default": ""}),
|
||||
"beauty_marks_custom": ("STRING", {"default": ""}),
|
||||
"genital_description_custom": ("STRING", {"default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("design_prompt", "feature_sheet", "adult_notes", "preset_status")
|
||||
FUNCTION = "design"
|
||||
CATEGORY = "NoxPrompter/Human"
|
||||
|
||||
def design(
|
||||
self,
|
||||
gender,
|
||||
height,
|
||||
face_type,
|
||||
body_type,
|
||||
body_shape,
|
||||
hair_length="Shoulder",
|
||||
hair_color="Deep Brunette",
|
||||
hair_style="Loose Waves",
|
||||
eye_detail="Almond Focus",
|
||||
eye_color="Amber Luminescence",
|
||||
skin_tone="Warm Honey",
|
||||
skin_detail="Soft Glow",
|
||||
beauty_marks="Bare",
|
||||
genital_description="Discrete",
|
||||
accent_features="",
|
||||
pose_hint="",
|
||||
consent_language="All subjects depicted are consenting adults aged 18+.",
|
||||
safety_addendum="",
|
||||
negative_prompt="",
|
||||
gender_custom="",
|
||||
height_custom="",
|
||||
face_type_custom="",
|
||||
body_type_custom="",
|
||||
body_shape_custom="",
|
||||
hair_length_custom="",
|
||||
hair_color_custom="",
|
||||
hair_style_custom="",
|
||||
eye_detail_custom="",
|
||||
eye_color_custom="",
|
||||
skin_tone_custom="",
|
||||
skin_detail_custom="",
|
||||
beauty_marks_custom="",
|
||||
genital_description_custom="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"gender": gender,
|
||||
"height": height,
|
||||
"face_type": face_type,
|
||||
"body_type": body_type,
|
||||
"body_shape": body_shape,
|
||||
"hair_length": hair_length,
|
||||
"hair_color": hair_color,
|
||||
"hair_style": hair_style,
|
||||
"eye_detail": eye_detail,
|
||||
"eye_color": eye_color,
|
||||
"skin_tone": skin_tone,
|
||||
"skin_detail": skin_detail,
|
||||
"beauty_marks": beauty_marks,
|
||||
"genital_description": genital_description,
|
||||
"accent_features": accent_features,
|
||||
"pose_hint": pose_hint,
|
||||
"consent_language": consent_language,
|
||||
"safety_addendum": safety_addendum,
|
||||
"negative_prompt": negative_prompt,
|
||||
"gender_custom": gender_custom,
|
||||
"height_custom": height_custom,
|
||||
"face_type_custom": face_type_custom,
|
||||
"body_type_custom": body_type_custom,
|
||||
"body_shape_custom": body_shape_custom,
|
||||
"hair_length_custom": hair_length_custom,
|
||||
"hair_color_custom": hair_color_custom,
|
||||
"hair_style_custom": hair_style_custom,
|
||||
"eye_detail_custom": eye_detail_custom,
|
||||
"eye_color_custom": eye_color_custom,
|
||||
"skin_tone_custom": skin_tone_custom,
|
||||
"skin_detail_custom": skin_detail_custom,
|
||||
"beauty_marks_custom": beauty_marks_custom,
|
||||
"genital_description_custom": genital_description_custom,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"human_designer",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
gender = config.get("gender", gender)
|
||||
height = config.get("height", height)
|
||||
face_type = config.get("face_type", face_type)
|
||||
body_type = config.get("body_type", body_type)
|
||||
body_shape = config.get("body_shape", body_shape)
|
||||
hair_length = config.get("hair_length", hair_length)
|
||||
hair_color = config.get("hair_color", hair_color)
|
||||
hair_style = config.get("hair_style", hair_style)
|
||||
eye_detail = config.get("eye_detail", eye_detail)
|
||||
eye_color = config.get("eye_color", eye_color)
|
||||
skin_tone = config.get("skin_tone", skin_tone)
|
||||
skin_detail = config.get("skin_detail", skin_detail)
|
||||
beauty_marks = config.get("beauty_marks", beauty_marks)
|
||||
genital_description = config.get("genital_description", genital_description)
|
||||
accent_features = config.get("accent_features", accent_features)
|
||||
pose_hint = config.get("pose_hint", pose_hint)
|
||||
consent_language = config.get("consent_language", consent_language)
|
||||
safety_addendum = config.get("safety_addendum", safety_addendum)
|
||||
negative_prompt = config.get("negative_prompt", negative_prompt)
|
||||
gender_custom = config.get("gender_custom", gender_custom)
|
||||
height_custom = config.get("height_custom", height_custom)
|
||||
face_type_custom = config.get("face_type_custom", face_type_custom)
|
||||
body_type_custom = config.get("body_type_custom", body_type_custom)
|
||||
body_shape_custom = config.get("body_shape_custom", body_shape_custom)
|
||||
hair_length_custom = config.get("hair_length_custom", hair_length_custom)
|
||||
hair_color_custom = config.get("hair_color_custom", hair_color_custom)
|
||||
hair_style_custom = config.get("hair_style_custom", hair_style_custom)
|
||||
eye_detail_custom = config.get("eye_detail_custom", eye_detail_custom)
|
||||
eye_color_custom = config.get("eye_color_custom", eye_color_custom)
|
||||
skin_tone_custom = config.get("skin_tone_custom", skin_tone_custom)
|
||||
skin_detail_custom = config.get("skin_detail_custom", skin_detail_custom)
|
||||
beauty_marks_custom = config.get("beauty_marks_custom", beauty_marks_custom)
|
||||
genital_description_custom = config.get("genital_description_custom", genital_description_custom)
|
||||
|
||||
gender_prompt, gender_notes = _resolve_with_custom(gender, gender_custom, HUMAN_DESIGN_GENDER_OPTIONS)
|
||||
height_prompt, height_notes = _resolve_with_custom(height, height_custom, HUMAN_DESIGN_HEIGHT_OPTIONS)
|
||||
face_prompt, face_notes = _resolve_with_custom(face_type, face_type_custom, HUMAN_DESIGN_FACE_OPTIONS)
|
||||
body_type_prompt, body_type_notes = _resolve_with_custom(body_type, body_type_custom, HUMAN_DESIGN_BODY_TYPE_OPTIONS)
|
||||
body_shape_prompt, body_shape_notes = _resolve_with_custom(body_shape, body_shape_custom, HUMAN_DESIGN_BODY_SHAPE_OPTIONS)
|
||||
hair_length_prompt, hair_length_notes = _resolve_with_custom(hair_length, hair_length_custom, HUMAN_DESIGN_HAIR_LENGTH_OPTIONS)
|
||||
hair_color_prompt, hair_color_notes = _resolve_with_custom(hair_color, hair_color_custom, HUMAN_DESIGN_HAIR_COLOR_OPTIONS)
|
||||
hair_style_prompt, hair_style_notes = _resolve_with_custom(hair_style, hair_style_custom, HUMAN_DESIGN_HAIR_STYLE_OPTIONS)
|
||||
eye_detail_prompt, eye_detail_notes = _resolve_with_custom(eye_detail, eye_detail_custom, HUMAN_DESIGN_EYE_DETAIL_OPTIONS)
|
||||
eye_color_prompt, eye_color_notes = _resolve_with_custom(eye_color, eye_color_custom, HUMAN_DESIGN_EYE_COLOR_OPTIONS)
|
||||
skin_tone_prompt, skin_tone_notes = _resolve_with_custom(skin_tone, skin_tone_custom, HUMAN_DESIGN_SKIN_TONE_OPTIONS)
|
||||
skin_detail_prompt, skin_detail_notes = _resolve_with_custom(skin_detail, skin_detail_custom, HUMAN_DESIGN_SKIN_DETAIL_OPTIONS)
|
||||
beauty_marks_prompt, beauty_marks_notes = _resolve_with_custom(beauty_marks, beauty_marks_custom, HUMAN_DESIGN_BEAUTY_MARK_OPTIONS)
|
||||
genital_prompt, genital_notes = _resolve_with_custom(genital_description, genital_description_custom, HUMAN_DESIGN_GENITAL_OPTIONS)
|
||||
|
||||
hair_components = [hair_length_prompt, hair_color_prompt, hair_style_prompt]
|
||||
hair_description = ", ".join(part for part in hair_components if part)
|
||||
|
||||
eye_components = [eye_detail_prompt, eye_color_prompt]
|
||||
eye_description = ", ".join(part for part in eye_components if part)
|
||||
|
||||
skin_components = [skin_tone_prompt, skin_detail_prompt]
|
||||
skin_description = ", ".join(part for part in skin_components if part)
|
||||
|
||||
def _as_sentence(text: str) -> str:
|
||||
cleaned = (text or "").strip()
|
||||
if not cleaned:
|
||||
return ""
|
||||
return cleaned if cleaned[-1] in ".!?" else f"{cleaned}."
|
||||
|
||||
core_segments = [gender_prompt, height_prompt, body_type_prompt, body_shape_prompt, face_prompt]
|
||||
core_description = ", ".join(part for part in core_segments if part)
|
||||
|
||||
prompt_fragments = []
|
||||
if core_description:
|
||||
prompt_fragments.append(core_description)
|
||||
if skin_description:
|
||||
prompt_fragments.append(f"Skin: {skin_description}")
|
||||
if eye_description:
|
||||
prompt_fragments.append(f"Eyes: {eye_description}")
|
||||
if hair_description:
|
||||
prompt_fragments.append(f"Hair: {hair_description}")
|
||||
if beauty_marks_prompt:
|
||||
prompt_fragments.append(f"Beauty marks: {beauty_marks_prompt}")
|
||||
if genital_prompt:
|
||||
prompt_fragments.append(f"Anatomy: {genital_prompt}")
|
||||
|
||||
accent_tokens = _split_tokens(accent_features)
|
||||
if accent_tokens:
|
||||
prompt_fragments.append("Accent features: " + ", ".join(dict.fromkeys(accent_tokens)))
|
||||
|
||||
if pose_hint.strip():
|
||||
prompt_fragments.append(f"Pose cue: {pose_hint.strip()}")
|
||||
|
||||
design_prompt = " ".join(filter(None, (_as_sentence(fragment) for fragment in prompt_fragments)))
|
||||
|
||||
feature_lines = [
|
||||
f"Gender/Sex: {gender_prompt}" if gender_prompt else "",
|
||||
f"Height: {height_prompt}" if height_prompt else "",
|
||||
f"Face Type: {face_prompt}" if face_prompt else "",
|
||||
f"Body Type: {body_type_prompt}" if body_type_prompt else "",
|
||||
f"Body Shape: {body_shape_prompt}" if body_shape_prompt else "",
|
||||
]
|
||||
if skin_description:
|
||||
feature_lines.append(f"Skin: {skin_description}")
|
||||
if hair_description:
|
||||
feature_lines.append(f"Hair: {hair_description}")
|
||||
if eye_description:
|
||||
feature_lines.append(f"Eyes: {eye_description}")
|
||||
if genital_prompt:
|
||||
feature_lines.append(f"Genital Description: {genital_prompt}")
|
||||
if beauty_marks_prompt:
|
||||
feature_lines.append(f"Beauty Marks: {beauty_marks_prompt}")
|
||||
if accent_tokens:
|
||||
feature_lines.append("Accent Features: " + ", ".join(dict.fromkeys(accent_tokens)))
|
||||
if pose_hint.strip():
|
||||
feature_lines.append(f"Pose Cue: {pose_hint.strip()}")
|
||||
feature_sheet = " | ".join(line for line in feature_lines if line)
|
||||
|
||||
adult_notes_parts = [
|
||||
"All descriptions apply to consenting adults aged 18+.",
|
||||
consent_language.strip(),
|
||||
]
|
||||
|
||||
curated_notes = [
|
||||
gender_notes,
|
||||
height_notes,
|
||||
face_notes,
|
||||
body_type_notes,
|
||||
body_shape_notes,
|
||||
hair_length_notes,
|
||||
hair_color_notes,
|
||||
hair_style_notes,
|
||||
eye_detail_notes,
|
||||
eye_color_notes,
|
||||
skin_tone_notes,
|
||||
skin_detail_notes,
|
||||
beauty_marks_notes,
|
||||
genital_notes,
|
||||
]
|
||||
adult_notes_parts.extend(note for note in curated_notes if note)
|
||||
if safety_addendum.strip():
|
||||
adult_notes_parts.append(safety_addendum.strip())
|
||||
if negative_prompt.strip():
|
||||
adult_notes_parts.append("Negative Prompt: " + negative_prompt.strip())
|
||||
|
||||
adult_notes = " | ".join(dict.fromkeys(part for part in adult_notes_parts if part))
|
||||
|
||||
return (design_prompt, feature_sheet, adult_notes, preset_status)
|
||||
|
||||
__all__ = ["NoxPromptHumanDesigner"]
|
||||
@@ -0,0 +1,226 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin, _resolve_with_custom
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptLightingMaster(PresetMixin):
|
||||
"""Construct cinematic lighting blueprints with mood and safety notes."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"blueprint": (list(LIGHTING_BLUEPRINT_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Three-Point"}),
|
||||
"key_style": (list(LIGHTING_KEY_STYLE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Softbox Key"}),
|
||||
},
|
||||
"optional": {
|
||||
"fill_style": (list(LIGHTING_FILL_STYLE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Soft Fill"}),
|
||||
"backlight": (list(LIGHTING_BACKLIGHT_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Rim Strip"}),
|
||||
"practical": (list(LIGHTING_PRACTICAL_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Lantern Cluster"}),
|
||||
"atmosphere": (list(LIGHTING_ATMOSPHERE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Haze"}),
|
||||
"color_gel": (list(LIGHTING_COLOR_GEL_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Teal & Orange"}),
|
||||
"camera_settings": (list(LIGHTING_CAM_SETTINGS.keys()) + [CUSTOM_OPTION], {"default": "Cinematic"}),
|
||||
"special_technique": (list(LIGHTING_SPECIAL_TECHNIQUES.keys()) + [CUSTOM_OPTION], {"default": "Shutter Drag"}),
|
||||
"energy_level": (list(LIGHTING_ENERGY_LEVEL_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Dynamic"}),
|
||||
"safety_profile": (list(["None"] + list(LIGHTING_SAFETY_NOTES.keys()) + [CUSTOM_OPTION]), {"default": "None"}),
|
||||
"subject_description": ("STRING", {"multiline": True, "default": ""}),
|
||||
"environment_description": ("STRING", {"multiline": True, "default": ""}),
|
||||
"accent_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"intensity_bias": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
"blueprint_custom": ("STRING", {"default": ""}),
|
||||
"key_style_custom": ("STRING", {"default": ""}),
|
||||
"fill_style_custom": ("STRING", {"default": ""}),
|
||||
"backlight_custom": ("STRING", {"default": ""}),
|
||||
"practical_custom": ("STRING", {"default": ""}),
|
||||
"atmosphere_custom": ("STRING", {"default": ""}),
|
||||
"color_gel_custom": ("STRING", {"default": ""}),
|
||||
"camera_settings_custom": ("STRING", {"default": ""}),
|
||||
"special_technique_custom": ("STRING", {"default": ""}),
|
||||
"energy_level_custom": ("STRING", {"default": ""}),
|
||||
"safety_profile_custom": ("STRING", {"default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("lighting_prompt", "mood_notes", "technical_notes", "preset_status")
|
||||
FUNCTION = "illuminate"
|
||||
CATEGORY = "NoxPrompter/Lighting"
|
||||
|
||||
def illuminate(
|
||||
self,
|
||||
blueprint,
|
||||
key_style,
|
||||
fill_style="Soft Fill",
|
||||
backlight="Rim Strip",
|
||||
practical="Lantern Cluster",
|
||||
atmosphere="Haze",
|
||||
color_gel="Teal & Orange",
|
||||
camera_settings="Cinematic",
|
||||
special_technique="Shutter Drag",
|
||||
energy_level="Dynamic",
|
||||
safety_profile="None",
|
||||
blueprint_custom="",
|
||||
key_style_custom="",
|
||||
fill_style_custom="",
|
||||
backlight_custom="",
|
||||
practical_custom="",
|
||||
atmosphere_custom="",
|
||||
color_gel_custom="",
|
||||
camera_settings_custom="",
|
||||
special_technique_custom="",
|
||||
energy_level_custom="",
|
||||
safety_profile_custom="",
|
||||
subject_description="",
|
||||
environment_description="",
|
||||
accent_notes="",
|
||||
intensity_bias=0.6,
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"blueprint": blueprint,
|
||||
"key_style": key_style,
|
||||
"fill_style": fill_style,
|
||||
"backlight": backlight,
|
||||
"practical": practical,
|
||||
"atmosphere": atmosphere,
|
||||
"color_gel": color_gel,
|
||||
"camera_settings": camera_settings,
|
||||
"special_technique": special_technique,
|
||||
"energy_level": energy_level,
|
||||
"safety_profile": safety_profile,
|
||||
"blueprint_custom": blueprint_custom,
|
||||
"key_style_custom": key_style_custom,
|
||||
"fill_style_custom": fill_style_custom,
|
||||
"backlight_custom": backlight_custom,
|
||||
"practical_custom": practical_custom,
|
||||
"atmosphere_custom": atmosphere_custom,
|
||||
"color_gel_custom": color_gel_custom,
|
||||
"camera_settings_custom": camera_settings_custom,
|
||||
"special_technique_custom": special_technique_custom,
|
||||
"energy_level_custom": energy_level_custom,
|
||||
"safety_profile_custom": safety_profile_custom,
|
||||
"subject_description": subject_description,
|
||||
"environment_description": environment_description,
|
||||
"accent_notes": accent_notes,
|
||||
"intensity_bias": intensity_bias,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"lighting_master",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
blueprint = config.get("blueprint", blueprint)
|
||||
key_style = config.get("key_style", key_style)
|
||||
fill_style = config.get("fill_style", fill_style)
|
||||
backlight = config.get("backlight", backlight)
|
||||
practical = config.get("practical", practical)
|
||||
atmosphere = config.get("atmosphere", atmosphere)
|
||||
color_gel = config.get("color_gel", color_gel)
|
||||
camera_settings = config.get("camera_settings", camera_settings)
|
||||
special_technique = config.get("special_technique", special_technique)
|
||||
energy_level = config.get("energy_level", energy_level)
|
||||
safety_profile = config.get("safety_profile", safety_profile)
|
||||
blueprint_custom = config.get("blueprint_custom", blueprint_custom)
|
||||
key_style_custom = config.get("key_style_custom", key_style_custom)
|
||||
fill_style_custom = config.get("fill_style_custom", fill_style_custom)
|
||||
backlight_custom = config.get("backlight_custom", backlight_custom)
|
||||
practical_custom = config.get("practical_custom", practical_custom)
|
||||
atmosphere_custom = config.get("atmosphere_custom", atmosphere_custom)
|
||||
color_gel_custom = config.get("color_gel_custom", color_gel_custom)
|
||||
camera_settings_custom = config.get("camera_settings_custom", camera_settings_custom)
|
||||
special_technique_custom = config.get("special_technique_custom", special_technique_custom)
|
||||
energy_level_custom = config.get("energy_level_custom", energy_level_custom)
|
||||
safety_profile_custom = config.get("safety_profile_custom", safety_profile_custom)
|
||||
subject_description = config.get("subject_description", subject_description)
|
||||
environment_description = config.get("environment_description", environment_description)
|
||||
accent_notes = config.get("accent_notes", accent_notes)
|
||||
intensity_bias = float(config.get("intensity_bias", intensity_bias))
|
||||
blueprint_prompt, blueprint_notes = _resolve_with_custom(blueprint, blueprint_custom, LIGHTING_BLUEPRINT_OPTIONS)
|
||||
key_prompt, key_notes = _resolve_with_custom(key_style, key_style_custom, LIGHTING_KEY_STYLE_OPTIONS)
|
||||
fill_prompt, fill_notes = _resolve_with_custom(fill_style, fill_style_custom, LIGHTING_FILL_STYLE_OPTIONS)
|
||||
back_prompt, back_notes = _resolve_with_custom(backlight, backlight_custom, LIGHTING_BACKLIGHT_OPTIONS)
|
||||
practical_prompt, practical_notes = _resolve_with_custom(practical, practical_custom, LIGHTING_PRACTICAL_OPTIONS)
|
||||
atmosphere_prompt, atmosphere_notes = _resolve_with_custom(atmosphere, atmosphere_custom, LIGHTING_ATMOSPHERE_OPTIONS)
|
||||
gel_prompt, gel_notes = _resolve_with_custom(color_gel, color_gel_custom, LIGHTING_COLOR_GEL_OPTIONS)
|
||||
camera_prompt, camera_notes = _resolve_with_custom(camera_settings, camera_settings_custom, LIGHTING_CAM_SETTINGS)
|
||||
technique_prompt, technique_notes = _resolve_with_custom(special_technique, special_technique_custom, LIGHTING_SPECIAL_TECHNIQUES)
|
||||
energy_prompt, energy_notes = _resolve_with_custom(energy_level, energy_level_custom, LIGHTING_ENERGY_LEVEL_OPTIONS)
|
||||
|
||||
intensity_text = self._describe_intensity(intensity_bias)
|
||||
|
||||
lighting_fragments = [
|
||||
blueprint_prompt,
|
||||
key_prompt,
|
||||
fill_prompt,
|
||||
back_prompt,
|
||||
practical_prompt,
|
||||
atmosphere_prompt,
|
||||
gel_prompt,
|
||||
technique_prompt,
|
||||
f"energy mode: {energy_prompt}" if energy_prompt else "",
|
||||
f"subject: {subject_description.strip()}" if subject_description.strip() else "",
|
||||
f"environment: {environment_description.strip()}" if environment_description.strip() else "",
|
||||
f"intensity bias: {intensity_text}",
|
||||
]
|
||||
lighting_prompt = ", ".join(fragment for fragment in lighting_fragments if fragment).strip()
|
||||
if lighting_prompt and lighting_prompt[-1] not in ".!?":
|
||||
lighting_prompt += "."
|
||||
if accent_notes.strip():
|
||||
lighting_prompt += f" Accents: {accent_notes.strip()}"
|
||||
|
||||
mood_sections = [
|
||||
f"Blueprint: {blueprint_notes}" if blueprint_notes else "",
|
||||
f"Key: {key_notes}" if key_notes else "",
|
||||
f"Fill: {fill_notes}" if fill_notes else "",
|
||||
f"Backlight: {back_notes}" if back_notes else "",
|
||||
f"Practical: {practical_notes}" if practical_notes else "",
|
||||
f"Atmosphere: {atmosphere_notes}" if atmosphere_notes else "",
|
||||
f"Gel: {gel_notes}" if gel_notes else "",
|
||||
f"Technique: {technique_notes}" if technique_notes else "",
|
||||
f"Energy: {energy_notes}" if energy_notes else "",
|
||||
]
|
||||
mood_notes = " | ".join(section for section in mood_sections if section)
|
||||
|
||||
technical_sections = [camera_prompt]
|
||||
if camera_notes:
|
||||
technical_sections.append(f"Camera Notes: {camera_notes}")
|
||||
safety_label = ""
|
||||
safety_text = ""
|
||||
if safety_profile == CUSTOM_OPTION:
|
||||
safety_label = (safety_profile_custom or "").strip()
|
||||
if safety_label:
|
||||
safety_text = safety_label
|
||||
elif safety_profile and safety_profile != "None":
|
||||
safety_label = safety_profile
|
||||
safety_text = LIGHTING_SAFETY_NOTES.get(safety_profile, "")
|
||||
if safety_label:
|
||||
if safety_text and safety_text != safety_label:
|
||||
technical_sections.append(f"Safety: {safety_label} — {safety_text}")
|
||||
else:
|
||||
technical_sections.append(f"Safety: {safety_label}")
|
||||
technical_notes = " | ".join(section for section in technical_sections if section)
|
||||
|
||||
return (lighting_prompt, mood_notes, technical_notes, preset_status)
|
||||
|
||||
def _describe_intensity(self, value: float) -> str:
|
||||
clamped = max(0.0, min(1.0, value))
|
||||
if clamped < 0.2:
|
||||
return "featherweight glow"
|
||||
if clamped < 0.4:
|
||||
return "gentle build"
|
||||
if clamped < 0.6:
|
||||
return "balanced exposure"
|
||||
if clamped < 0.8:
|
||||
return "pronounced contrast"
|
||||
if clamped < 0.95:
|
||||
return "high drama punch"
|
||||
return "extreme spectacle"
|
||||
|
||||
__all__ = ["NoxPromptLightingMaster"]
|
||||
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptNarrativeWeaver(PresetMixin):
|
||||
"""Craft narrative scaffolding for the prompt builder."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"hero_archetype": (list(NARRATIVE_ARCHETYPES.keys()), {"default": "Resolute Protector"}),
|
||||
"story_tone": (list(NARRATIVE_TONES.keys()), {"default": "Hopeful Resurgence"}),
|
||||
"environment": (list(NARRATIVE_ENVIRONMENTS.keys()), {"default": "Rain-Soaked Citadel"}),
|
||||
},
|
||||
"optional": {
|
||||
"set_piece": (list(NARRATIVE_SETPIECES.keys()), {"default": "Climactic Duel"}),
|
||||
"tempo": (list(TEMPO_PROFILES.keys()), {"default": "surging"}),
|
||||
"focus_detail": ("STRING", {"multiline": True, "default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = (
|
||||
"subject_focus",
|
||||
"scene_setting",
|
||||
"motion_arc",
|
||||
"narrative_hook",
|
||||
"extra_descriptors",
|
||||
"preset_status",
|
||||
)
|
||||
FUNCTION = "weave"
|
||||
CATEGORY = "NoxPrompter/Companions"
|
||||
|
||||
def weave(
|
||||
self,
|
||||
hero_archetype,
|
||||
story_tone,
|
||||
environment,
|
||||
set_piece="Climactic Duel",
|
||||
tempo="surging",
|
||||
focus_detail="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"hero_archetype": hero_archetype,
|
||||
"story_tone": story_tone,
|
||||
"environment": environment,
|
||||
"set_piece": set_piece,
|
||||
"tempo": tempo,
|
||||
"focus_detail": focus_detail,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"narrative_weaver",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
hero_archetype = config.get("hero_archetype", "Resolute Protector")
|
||||
story_tone = config.get("story_tone", "Hopeful Resurgence")
|
||||
environment = config.get("environment", "Rain-Soaked Citadel")
|
||||
set_piece = config.get("set_piece", "Climactic Duel")
|
||||
tempo = config.get("tempo", "surging")
|
||||
focus_detail = config.get("focus_detail", "")
|
||||
|
||||
archetype = NARRATIVE_ARCHETYPES.get(hero_archetype, {"subject": hero_archetype, "keywords": []})
|
||||
tone = NARRATIVE_TONES.get(story_tone, {"descriptor": story_tone, "ambience": "", "hook": "", "keywords": []})
|
||||
env = NARRATIVE_ENVIRONMENTS.get(environment, {"description": environment, "details": "", "keywords": []})
|
||||
setpiece = NARRATIVE_SETPIECES.get(set_piece, {"motion": set_piece, "hook": "", "keywords": []})
|
||||
tempo_cfg = TEMPO_PROFILES.get(tempo, TEMPO_PROFILES["measured build"])
|
||||
|
||||
subject_parts = [archetype.get("subject", "")]
|
||||
if tempo_cfg.get("subject_accent"):
|
||||
subject_parts.append(tempo_cfg["subject_accent"])
|
||||
if tone.get("descriptor"):
|
||||
subject_parts.append(tone["descriptor"])
|
||||
subject_focus = ", ".join(part for part in subject_parts if part).strip(", ")
|
||||
|
||||
scene_parts = [env.get("description", ""), env.get("details", ""), tone.get("ambience", "")]
|
||||
scene_setting = ", ".join(part for part in scene_parts if part).strip(", ")
|
||||
|
||||
motion_parts = [tempo_cfg.get("motion_prefix"), setpiece.get("motion"), tempo_cfg.get("motion_suffix")]
|
||||
motion_arc = " ".join(part.strip() for part in motion_parts if part and part.strip())
|
||||
|
||||
hook_parts = [tone.get("hook"), setpiece.get("hook"), tempo_cfg.get("hook_suffix")]
|
||||
narrative_hook = " ".join(part.strip() for part in hook_parts if part and part.strip())
|
||||
|
||||
descriptor_sources = [
|
||||
archetype.get("keywords", []),
|
||||
tone.get("keywords", []),
|
||||
env.get("keywords", []),
|
||||
setpiece.get("keywords", []),
|
||||
tempo_cfg.get("keywords", []),
|
||||
]
|
||||
if focus_detail.strip():
|
||||
descriptor_sources.append(self._split_keywords(focus_detail))
|
||||
|
||||
descriptors = []
|
||||
for source in descriptor_sources:
|
||||
descriptors.extend(source)
|
||||
descriptors = list(dict.fromkeys(desc.strip() for desc in descriptors if desc and desc.strip()))
|
||||
extra_descriptors = ", ".join(descriptors)
|
||||
|
||||
return (
|
||||
subject_focus,
|
||||
scene_setting,
|
||||
motion_arc,
|
||||
narrative_hook,
|
||||
extra_descriptors,
|
||||
preset_status,
|
||||
)
|
||||
|
||||
def _split_keywords(self, text):
|
||||
if not text:
|
||||
return []
|
||||
separators = [",", "\n", ";"]
|
||||
tokens = [text]
|
||||
for sep in separators:
|
||||
temp = []
|
||||
for token in tokens:
|
||||
temp.extend(token.split(sep))
|
||||
tokens = temp
|
||||
return [token.strip() for token in tokens if token.strip()]
|
||||
|
||||
__all__ = ["NoxPromptNarrativeWeaver"]
|
||||
@@ -0,0 +1,230 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin, _resolve_action_option, _split_tokens
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptNSFWActionDirector(PresetMixin):
|
||||
"""Stage adult-oriented action performances with explicit safety framing."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"primary_action": (list(NSFW_ACTION_PRIMARY_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Sensual Caress"}),
|
||||
},
|
||||
"optional": {
|
||||
"secondary_action": (list(NSFW_ACTION_SECONDARY_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "None"}),
|
||||
"tempo": (list(NSFW_ACTION_TEMPO_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Slow Tease"}),
|
||||
"interaction_mode": (list(NSFW_ACTION_INTERACTION_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Solo Self-Admiration"}),
|
||||
"props": ("STRING", {"multiline": True, "default": ""}),
|
||||
"environment_mood": ("STRING", {"multiline": True, "default": ""}),
|
||||
"heat_descriptor": ("STRING", {"default": ""}),
|
||||
"camera_language": ("STRING", {"multiline": True, "default": ""}),
|
||||
"soundscape": ("STRING", {"multiline": True, "default": ""}),
|
||||
"dialogue_hook": ("STRING", {"multiline": True, "default": ""}),
|
||||
"consent_focus": ("STRING", {"multiline": True, "default": "enthusiastic consent, explicit verbal check-ins"}),
|
||||
"boundary_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"aftercare_plan": ("STRING", {"multiline": True, "default": ""}),
|
||||
"safety_props": ("STRING", {"multiline": True, "default": ""}),
|
||||
"beat_emphasis": ("STRING", {"multiline": True, "default": ""}),
|
||||
"risk_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"primary_action_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"secondary_action_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tempo_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"interaction_mode_custom": ("STRING", {"multiline": True, "default": ""}),
|
||||
"primary_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"secondary_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tempo_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"interaction_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"include_safety_note": ("BOOLEAN", {"default": True}),
|
||||
"custom_safety_note": ("STRING", {"multiline": True, "default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("action_prompt", "action_summary", "safety_notes", "preset_status")
|
||||
FUNCTION = "direct"
|
||||
CATEGORY = "NoxPrompter/NSFW"
|
||||
|
||||
def direct(
|
||||
self,
|
||||
primary_action,
|
||||
secondary_action="None",
|
||||
tempo="Slow Tease",
|
||||
interaction_mode="Solo Self-Admiration",
|
||||
props="",
|
||||
environment_mood="",
|
||||
heat_descriptor="",
|
||||
camera_language="",
|
||||
soundscape="",
|
||||
dialogue_hook="",
|
||||
consent_focus="enthusiastic consent, explicit verbal check-ins",
|
||||
boundary_notes="",
|
||||
aftercare_plan="",
|
||||
safety_props="",
|
||||
beat_emphasis="",
|
||||
risk_notes="",
|
||||
primary_action_custom="",
|
||||
secondary_action_custom="",
|
||||
tempo_custom="",
|
||||
interaction_mode_custom="",
|
||||
primary_notes="",
|
||||
secondary_notes="",
|
||||
tempo_notes="",
|
||||
interaction_notes="",
|
||||
include_safety_note=True,
|
||||
custom_safety_note="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"primary_action": primary_action,
|
||||
"secondary_action": secondary_action,
|
||||
"tempo": tempo,
|
||||
"interaction_mode": interaction_mode,
|
||||
"props": props,
|
||||
"environment_mood": environment_mood,
|
||||
"heat_descriptor": heat_descriptor,
|
||||
"camera_language": camera_language,
|
||||
"soundscape": soundscape,
|
||||
"dialogue_hook": dialogue_hook,
|
||||
"consent_focus": consent_focus,
|
||||
"boundary_notes": boundary_notes,
|
||||
"aftercare_plan": aftercare_plan,
|
||||
"safety_props": safety_props,
|
||||
"beat_emphasis": beat_emphasis,
|
||||
"risk_notes": risk_notes,
|
||||
"primary_action_custom": primary_action_custom,
|
||||
"secondary_action_custom": secondary_action_custom,
|
||||
"tempo_custom": tempo_custom,
|
||||
"interaction_mode_custom": interaction_mode_custom,
|
||||
"primary_notes": primary_notes,
|
||||
"secondary_notes": secondary_notes,
|
||||
"tempo_notes": tempo_notes,
|
||||
"interaction_notes": interaction_notes,
|
||||
"include_safety_note": include_safety_note,
|
||||
"custom_safety_note": custom_safety_note,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"nsfw_action_director",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
primary_action = config.get("primary_action", primary_action)
|
||||
secondary_action = config.get("secondary_action", secondary_action)
|
||||
tempo = config.get("tempo", tempo)
|
||||
interaction_mode = config.get("interaction_mode", interaction_mode)
|
||||
props = config.get("props", props)
|
||||
environment_mood = config.get("environment_mood", environment_mood)
|
||||
heat_descriptor = config.get("heat_descriptor", heat_descriptor)
|
||||
camera_language = config.get("camera_language", camera_language)
|
||||
soundscape = config.get("soundscape", soundscape)
|
||||
dialogue_hook = config.get("dialogue_hook", dialogue_hook)
|
||||
consent_focus = config.get("consent_focus", consent_focus)
|
||||
boundary_notes = config.get("boundary_notes", boundary_notes)
|
||||
aftercare_plan = config.get("aftercare_plan", aftercare_plan)
|
||||
safety_props = config.get("safety_props", safety_props)
|
||||
beat_emphasis = config.get("beat_emphasis", beat_emphasis)
|
||||
risk_notes = config.get("risk_notes", risk_notes)
|
||||
primary_action_custom = config.get("primary_action_custom", primary_action_custom)
|
||||
secondary_action_custom = config.get("secondary_action_custom", secondary_action_custom)
|
||||
tempo_custom = config.get("tempo_custom", tempo_custom)
|
||||
interaction_mode_custom = config.get("interaction_mode_custom", interaction_mode_custom)
|
||||
primary_notes = config.get("primary_notes", primary_notes)
|
||||
secondary_notes = config.get("secondary_notes", secondary_notes)
|
||||
tempo_notes = config.get("tempo_notes", tempo_notes)
|
||||
interaction_notes = config.get("interaction_notes", interaction_notes)
|
||||
include_safety_note = bool(config.get("include_safety_note", include_safety_note))
|
||||
custom_safety_note = config.get("custom_safety_note", custom_safety_note)
|
||||
|
||||
primary_prompt, primary_summary = _resolve_action_option(primary_action, primary_action_custom, NSFW_ACTION_PRIMARY_OPTIONS)
|
||||
secondary_prompt, secondary_summary = _resolve_action_option(secondary_action, secondary_action_custom, NSFW_ACTION_SECONDARY_OPTIONS)
|
||||
tempo_prompt, tempo_summary = _resolve_action_option(tempo, tempo_custom, NSFW_ACTION_TEMPO_OPTIONS)
|
||||
interaction_prompt, interaction_summary = _resolve_action_option(interaction_mode, interaction_mode_custom, NSFW_ACTION_INTERACTION_OPTIONS)
|
||||
|
||||
props_list = _split_tokens(props)
|
||||
beat_highlights = _split_tokens(beat_emphasis)
|
||||
risk_list = _split_tokens(risk_notes)
|
||||
safety_tool_list = _split_tokens(safety_props)
|
||||
|
||||
prompt_parts = [primary_prompt]
|
||||
for fragment in (secondary_prompt, tempo_prompt, interaction_prompt):
|
||||
if fragment:
|
||||
prompt_parts.append(fragment)
|
||||
if heat_descriptor.strip():
|
||||
prompt_parts.append(heat_descriptor.strip())
|
||||
if environment_mood.strip():
|
||||
prompt_parts.append(f"set mood: {environment_mood.strip()}")
|
||||
if props_list:
|
||||
prompt_parts.append("props: " + ", ".join(props_list))
|
||||
action_prompt = ", ".join(part for part in prompt_parts if part)
|
||||
if action_prompt and action_prompt[-1] not in ".!?":
|
||||
action_prompt += "."
|
||||
if camera_language.strip():
|
||||
action_prompt += f" Camera language: {camera_language.strip()}"
|
||||
if soundscape.strip():
|
||||
action_prompt += f" Soundscape: {soundscape.strip()}"
|
||||
if dialogue_hook.strip():
|
||||
action_prompt += f" Dialogue cue: {dialogue_hook.strip()}"
|
||||
|
||||
summary_segments = []
|
||||
if primary_summary:
|
||||
summary_segments.append(f"Primary: {primary_summary}")
|
||||
if primary_notes.strip():
|
||||
summary_segments.append(primary_notes.strip())
|
||||
if secondary_summary:
|
||||
summary_segments.append(f"Secondary: {secondary_summary}")
|
||||
if secondary_notes.strip():
|
||||
summary_segments.append(secondary_notes.strip())
|
||||
if tempo_summary:
|
||||
summary_segments.append(f"Tempo: {tempo_summary}")
|
||||
if tempo_notes.strip():
|
||||
summary_segments.append(tempo_notes.strip())
|
||||
if interaction_summary:
|
||||
summary_segments.append(f"Interaction: {interaction_summary}")
|
||||
if interaction_notes.strip():
|
||||
summary_segments.append(interaction_notes.strip())
|
||||
if heat_descriptor.strip():
|
||||
summary_segments.append(f"Heat: {heat_descriptor.strip()}")
|
||||
if props_list:
|
||||
summary_segments.append("Props: " + ", ".join(props_list))
|
||||
if beat_highlights:
|
||||
summary_segments.append("Beat focus: " + ", ".join(beat_highlights))
|
||||
if environment_mood.strip():
|
||||
summary_segments.append(f"Environment: {environment_mood.strip()}")
|
||||
if camera_language.strip():
|
||||
summary_segments.append("Camera: " + camera_language.strip())
|
||||
if soundscape.strip():
|
||||
summary_segments.append("Sound: " + soundscape.strip())
|
||||
if dialogue_hook.strip():
|
||||
summary_segments.append("Dialogue: " + dialogue_hook.strip())
|
||||
if consent_focus.strip():
|
||||
summary_segments.append("Consent: " + consent_focus.strip())
|
||||
action_summary = " | ".join(segment for segment in summary_segments if segment)
|
||||
|
||||
safety_parts = []
|
||||
if include_safety_note:
|
||||
safety_parts.append(NSFW_DEFAULT_SAFETY_NOTE)
|
||||
if consent_focus.strip():
|
||||
safety_parts.append("Consent focus: " + consent_focus.strip())
|
||||
if boundary_notes.strip():
|
||||
safety_parts.append("Boundaries: " + boundary_notes.strip())
|
||||
if risk_list:
|
||||
safety_parts.append("Risks monitored: " + ", ".join(risk_list))
|
||||
if safety_tool_list:
|
||||
safety_parts.append("Safety tools: " + ", ".join(safety_tool_list))
|
||||
if aftercare_plan.strip():
|
||||
safety_parts.append("Aftercare: " + aftercare_plan.strip())
|
||||
if custom_safety_note.strip():
|
||||
safety_parts.append(custom_safety_note.strip())
|
||||
safety_notes = " | ".join(part for part in safety_parts if part)
|
||||
|
||||
return (action_prompt.strip(), action_summary, safety_notes, preset_status)
|
||||
|
||||
__all__ = ["NoxPromptNSFWActionDirector"]
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from ..common import _split_tokens
|
||||
from ..constants import NSFW_BASELINE_NEGATIVES, NSFW_DEFAULT_SAFETY_NOTE
|
||||
|
||||
|
||||
class NoxPromptNSFWAIO:
|
||||
"""Combine NSFW designer and action fragments into a single, safety-conscious output."""
|
||||
|
||||
CATEGORY = "NoxPrompter/NSFW"
|
||||
FUNCTION = "combine"
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("combined_prompt", "combined_negative", "combined_safety", "fusion_notes")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"designer_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"designer_negative": ("STRING", {"multiline": True, "default": ""}),
|
||||
"designer_safety": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
"action_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"action_summary": ("STRING", {"multiline": True, "default": ""}),
|
||||
"action_safety": ("STRING", {"multiline": True, "default": ""}),
|
||||
"camera_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"extra_positive": ("STRING", {"multiline": True, "default": ""}),
|
||||
"extra_negative": ("STRING", {"multiline": True, "default": ""}),
|
||||
"extra_safety": ("STRING", {"multiline": True, "default": ""}),
|
||||
"include_baseline_negatives": ("BOOLEAN", {"default": True}),
|
||||
"include_default_safety": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
def combine(
|
||||
self,
|
||||
designer_prompt: str,
|
||||
designer_negative: str,
|
||||
designer_safety: str,
|
||||
action_prompt: str = "",
|
||||
action_summary: str = "",
|
||||
action_safety: str = "",
|
||||
camera_notes: str = "",
|
||||
extra_positive: str = "",
|
||||
extra_negative: str = "",
|
||||
extra_safety: str = "",
|
||||
include_baseline_negatives: bool = True,
|
||||
include_default_safety: bool = True,
|
||||
):
|
||||
positive_sections = [
|
||||
designer_prompt.strip(),
|
||||
action_prompt.strip(),
|
||||
extra_positive.strip(),
|
||||
]
|
||||
combined_prompt = "\n".join([section for section in positive_sections if section]).strip()
|
||||
|
||||
negative_tokens: List[str] = []
|
||||
for block in (designer_negative, extra_negative):
|
||||
if not block:
|
||||
continue
|
||||
for token in _split_tokens(block.replace("\n", ",")):
|
||||
cleaned = token.strip()
|
||||
if cleaned:
|
||||
negative_tokens.append(cleaned)
|
||||
if include_baseline_negatives:
|
||||
negative_tokens.extend(NSFW_BASELINE_NEGATIVES)
|
||||
combined_negative = ", ".join(self._unique_sequence(negative_tokens))
|
||||
|
||||
safety_chunks: List[str] = []
|
||||
if include_default_safety:
|
||||
safety_chunks.append(NSFW_DEFAULT_SAFETY_NOTE)
|
||||
safety_chunks.extend([designer_safety, action_safety, extra_safety])
|
||||
combined_safety = "\n".join(self._unique_sequence(self._split_lines(safety_chunks))).strip()
|
||||
|
||||
fusion_lines: List[str] = []
|
||||
if action_summary.strip():
|
||||
fusion_lines.append(f"Action Summary: {action_summary.strip()}")
|
||||
if camera_notes.strip():
|
||||
fusion_lines.append(f"Camera Notes: {camera_notes.strip()}")
|
||||
if extra_positive.strip():
|
||||
fusion_lines.append("Extras merged into prompt body.")
|
||||
if extra_negative.strip():
|
||||
fusion_lines.append("Extra negative cues merged into combined negatives.")
|
||||
fusion_notes = "\n".join(fusion_lines)
|
||||
|
||||
return combined_prompt, combined_negative, combined_safety, fusion_notes
|
||||
|
||||
@staticmethod
|
||||
def _split_lines(chunks: List[str]) -> List[str]:
|
||||
lines: List[str] = []
|
||||
for chunk in chunks:
|
||||
if not chunk:
|
||||
continue
|
||||
for line in chunk.replace("\r", "\n").split("\n"):
|
||||
cleaned = line.strip()
|
||||
if cleaned:
|
||||
lines.append(cleaned)
|
||||
return lines
|
||||
|
||||
@staticmethod
|
||||
def _unique_sequence(values: List[str]) -> List[str]:
|
||||
seen = set()
|
||||
ordered: List[str] = []
|
||||
for value in values:
|
||||
key = value.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
ordered.append(value)
|
||||
return ordered
|
||||
|
||||
|
||||
__all__ = ["NoxPromptNSFWAIO"]
|
||||
@@ -0,0 +1,457 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptNSFWDesigner(PresetMixin):
|
||||
"""Assemble mature-oriented prompts with responsible defaults and safeguards."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"subject_focus": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": "Confident adult model with poised expression",
|
||||
},
|
||||
),
|
||||
"scene_setting": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": "Softly lit studio lounge accented with plush textures",
|
||||
},
|
||||
),
|
||||
"pose_profile": (list(NSFW_POSE_PROFILES.keys()), {"default": "Suggestive Pinup"}),
|
||||
"wardrobe_style": (
|
||||
list(NSFW_WARDROBE_STYLES.keys()),
|
||||
{"default": "Silk Lingerie"},
|
||||
),
|
||||
"tone_profile": (list(NSFW_TONE_MOODS.keys()), {"default": "Glamorous Editorial"}),
|
||||
},
|
||||
"optional": {
|
||||
"lighting_setup": (
|
||||
list(NSFW_LIGHTING_SETUPS.keys()),
|
||||
{"default": "Candlelit Warmth"},
|
||||
),
|
||||
"camera_framing": (
|
||||
list(NSFW_CAMERA_FRAMING.keys()),
|
||||
{"default": "Intimate Portrait"},
|
||||
),
|
||||
"explicitness_level": (
|
||||
list(NSFW_EXPLICITNESS_LEVELS.keys()),
|
||||
{"default": "Implied Nude"},
|
||||
),
|
||||
"set_design": (
|
||||
list(NSFW_SET_DESIGNS.keys()),
|
||||
{"default": "None"},
|
||||
),
|
||||
"heat_profile": (
|
||||
list(NSFW_HEAT_LEVELS.keys()),
|
||||
{"default": "Sensual"},
|
||||
),
|
||||
"custom_pose_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_pose_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_wardrobe_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_wardrobe_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_tone_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_tone_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_lighting_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_lighting_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_camera_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_camera_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_set_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_set_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_explicitness_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_explicitness_negative": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_explicitness_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_heat_prompt": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_heat_notes": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"custom_heat_negative": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"detail_accent": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": "soft blemish-free skin, natural curves, respectful staging",
|
||||
},
|
||||
),
|
||||
"nsfw_tags": ("STRING", {"multiline": True, "default": "tasteful, mature, confident"}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"include_negative_baseline": ("BOOLEAN", {"default": True}),
|
||||
"include_safety_note": ("BOOLEAN", {"default": True}),
|
||||
"custom_safety_note": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("prompt", "negative_prompt", "safety_notes", "preset_status")
|
||||
FUNCTION = "design_prompt"
|
||||
CATEGORY = "NoxPrompter/NSFW"
|
||||
|
||||
def design_prompt(
|
||||
self,
|
||||
subject_focus,
|
||||
scene_setting,
|
||||
pose_profile,
|
||||
wardrobe_style,
|
||||
tone_profile,
|
||||
lighting_setup="Candlelit Warmth",
|
||||
camera_framing="Intimate Portrait",
|
||||
explicitness_level="Implied Nude",
|
||||
set_design="None",
|
||||
heat_profile="Sensual",
|
||||
detail_accent="",
|
||||
nsfw_tags="",
|
||||
negative_prompt="",
|
||||
include_negative_baseline=True,
|
||||
include_safety_note=True,
|
||||
custom_pose_prompt="",
|
||||
custom_pose_notes="",
|
||||
custom_wardrobe_prompt="",
|
||||
custom_wardrobe_notes="",
|
||||
custom_tone_prompt="",
|
||||
custom_tone_notes="",
|
||||
custom_lighting_prompt="",
|
||||
custom_lighting_notes="",
|
||||
custom_camera_prompt="",
|
||||
custom_camera_notes="",
|
||||
custom_set_prompt="",
|
||||
custom_set_notes="",
|
||||
custom_explicitness_prompt="",
|
||||
custom_explicitness_negative="",
|
||||
custom_explicitness_notes="",
|
||||
custom_safety_note="",
|
||||
custom_heat_prompt="",
|
||||
custom_heat_notes="",
|
||||
custom_heat_negative="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"subject_focus": subject_focus,
|
||||
"scene_setting": scene_setting,
|
||||
"pose_profile": pose_profile,
|
||||
"wardrobe_style": wardrobe_style,
|
||||
"tone_profile": tone_profile,
|
||||
"lighting_setup": lighting_setup,
|
||||
"camera_framing": camera_framing,
|
||||
"explicitness_level": explicitness_level,
|
||||
"set_design": set_design,
|
||||
"heat_profile": heat_profile,
|
||||
"detail_accent": detail_accent,
|
||||
"nsfw_tags": nsfw_tags,
|
||||
"negative_prompt": negative_prompt,
|
||||
"include_negative_baseline": include_negative_baseline,
|
||||
"include_safety_note": include_safety_note,
|
||||
"custom_pose_prompt": custom_pose_prompt,
|
||||
"custom_pose_notes": custom_pose_notes,
|
||||
"custom_wardrobe_prompt": custom_wardrobe_prompt,
|
||||
"custom_wardrobe_notes": custom_wardrobe_notes,
|
||||
"custom_tone_prompt": custom_tone_prompt,
|
||||
"custom_tone_notes": custom_tone_notes,
|
||||
"custom_lighting_prompt": custom_lighting_prompt,
|
||||
"custom_lighting_notes": custom_lighting_notes,
|
||||
"custom_camera_prompt": custom_camera_prompt,
|
||||
"custom_camera_notes": custom_camera_notes,
|
||||
"custom_set_prompt": custom_set_prompt,
|
||||
"custom_set_notes": custom_set_notes,
|
||||
"custom_explicitness_prompt": custom_explicitness_prompt,
|
||||
"custom_explicitness_negative": custom_explicitness_negative,
|
||||
"custom_explicitness_notes": custom_explicitness_notes,
|
||||
"custom_safety_note": custom_safety_note,
|
||||
"custom_heat_prompt": custom_heat_prompt,
|
||||
"custom_heat_notes": custom_heat_notes,
|
||||
"custom_heat_negative": custom_heat_negative,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"nsfw_designer",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
subject_focus = config.get("subject_focus", subject_focus)
|
||||
scene_setting = config.get("scene_setting", scene_setting)
|
||||
pose_profile = config.get("pose_profile", pose_profile)
|
||||
wardrobe_style = config.get("wardrobe_style", wardrobe_style)
|
||||
tone_profile = config.get("tone_profile", tone_profile)
|
||||
lighting_setup = config.get("lighting_setup", lighting_setup)
|
||||
camera_framing = config.get("camera_framing", camera_framing)
|
||||
explicitness_level = config.get("explicitness_level", explicitness_level)
|
||||
set_design = config.get("set_design", set_design)
|
||||
heat_profile = config.get("heat_profile", heat_profile)
|
||||
detail_accent = config.get("detail_accent", detail_accent)
|
||||
nsfw_tags = config.get("nsfw_tags", nsfw_tags)
|
||||
negative_prompt = config.get("negative_prompt", negative_prompt)
|
||||
include_negative_baseline = bool(config.get("include_negative_baseline", include_negative_baseline))
|
||||
include_safety_note = bool(config.get("include_safety_note", include_safety_note))
|
||||
custom_pose_prompt = config.get("custom_pose_prompt", custom_pose_prompt)
|
||||
custom_pose_notes = config.get("custom_pose_notes", custom_pose_notes)
|
||||
custom_wardrobe_prompt = config.get("custom_wardrobe_prompt", custom_wardrobe_prompt)
|
||||
custom_wardrobe_notes = config.get("custom_wardrobe_notes", custom_wardrobe_notes)
|
||||
custom_tone_prompt = config.get("custom_tone_prompt", custom_tone_prompt)
|
||||
custom_tone_notes = config.get("custom_tone_notes", custom_tone_notes)
|
||||
custom_lighting_prompt = config.get("custom_lighting_prompt", custom_lighting_prompt)
|
||||
custom_lighting_notes = config.get("custom_lighting_notes", custom_lighting_notes)
|
||||
custom_camera_prompt = config.get("custom_camera_prompt", custom_camera_prompt)
|
||||
custom_camera_notes = config.get("custom_camera_notes", custom_camera_notes)
|
||||
custom_set_prompt = config.get("custom_set_prompt", custom_set_prompt)
|
||||
custom_set_notes = config.get("custom_set_notes", custom_set_notes)
|
||||
custom_explicitness_prompt = config.get("custom_explicitness_prompt", custom_explicitness_prompt)
|
||||
custom_explicitness_negative = config.get("custom_explicitness_negative", custom_explicitness_negative)
|
||||
custom_explicitness_notes = config.get("custom_explicitness_notes", custom_explicitness_notes)
|
||||
custom_safety_note = config.get("custom_safety_note", custom_safety_note)
|
||||
custom_heat_prompt = config.get("custom_heat_prompt", custom_heat_prompt)
|
||||
custom_heat_notes = config.get("custom_heat_notes", custom_heat_notes)
|
||||
custom_heat_negative = config.get("custom_heat_negative", custom_heat_negative)
|
||||
|
||||
pose_cfg = self._resolve_nsfw_option(
|
||||
pose_profile,
|
||||
NSFW_POSE_PROFILES,
|
||||
custom_prompt=custom_pose_prompt,
|
||||
custom_notes=custom_pose_notes,
|
||||
)
|
||||
wardrobe_cfg = self._resolve_nsfw_option(
|
||||
wardrobe_style,
|
||||
NSFW_WARDROBE_STYLES,
|
||||
custom_prompt=custom_wardrobe_prompt,
|
||||
custom_notes=custom_wardrobe_notes,
|
||||
)
|
||||
tone_cfg = self._resolve_nsfw_option(
|
||||
tone_profile,
|
||||
NSFW_TONE_MOODS,
|
||||
custom_prompt=custom_tone_prompt,
|
||||
custom_notes=custom_tone_notes,
|
||||
)
|
||||
lighting_cfg = self._resolve_nsfw_option(
|
||||
lighting_setup,
|
||||
NSFW_LIGHTING_SETUPS,
|
||||
custom_prompt=custom_lighting_prompt,
|
||||
custom_notes=custom_lighting_notes,
|
||||
)
|
||||
camera_cfg = self._resolve_nsfw_option(
|
||||
camera_framing,
|
||||
NSFW_CAMERA_FRAMING,
|
||||
custom_prompt=custom_camera_prompt,
|
||||
custom_notes=custom_camera_notes,
|
||||
)
|
||||
explicit_cfg = self._resolve_nsfw_option(
|
||||
explicitness_level,
|
||||
NSFW_EXPLICITNESS_LEVELS,
|
||||
custom_prompt=custom_explicitness_prompt,
|
||||
custom_notes=custom_explicitness_notes,
|
||||
custom_negative=custom_explicitness_negative,
|
||||
)
|
||||
set_cfg = self._resolve_nsfw_option(
|
||||
set_design,
|
||||
NSFW_SET_DESIGNS,
|
||||
custom_prompt=custom_set_prompt,
|
||||
custom_notes=custom_set_notes,
|
||||
)
|
||||
heat_cfg = self._resolve_nsfw_option(
|
||||
heat_profile,
|
||||
NSFW_HEAT_LEVELS,
|
||||
custom_prompt=custom_heat_prompt,
|
||||
custom_notes=custom_heat_notes,
|
||||
custom_negative=custom_heat_negative,
|
||||
)
|
||||
|
||||
fragments = []
|
||||
|
||||
subject_clause_parts = [subject_focus.strip()]
|
||||
if tone_cfg.get("prompt"):
|
||||
subject_clause_parts.append(tone_cfg["prompt"])
|
||||
if explicit_cfg.get("prompt"):
|
||||
subject_clause_parts.append(explicit_cfg["prompt"])
|
||||
if heat_cfg.get("prompt"):
|
||||
subject_clause_parts.append(heat_cfg["prompt"])
|
||||
subject_clause = ", ".join(part for part in subject_clause_parts if part)
|
||||
if subject_clause:
|
||||
fragments.append(subject_clause)
|
||||
|
||||
scene_fragments = []
|
||||
if scene_setting.strip():
|
||||
scene_fragments.append(scene_setting.strip())
|
||||
set_prompt_text = set_cfg.get("prompt")
|
||||
if set_prompt_text:
|
||||
scene_fragments.append(set_prompt_text)
|
||||
if scene_fragments:
|
||||
fragments.append(". ".join(scene_fragments))
|
||||
|
||||
descriptive_chunks = [
|
||||
pose_cfg.get("prompt", ""),
|
||||
wardrobe_cfg.get("prompt", ""),
|
||||
lighting_cfg.get("prompt", ""),
|
||||
camera_cfg.get("prompt", ""),
|
||||
]
|
||||
if detail_accent.strip():
|
||||
descriptive_chunks.append(detail_accent.strip())
|
||||
descriptive_line = ", ".join(chunk for chunk in descriptive_chunks if chunk)
|
||||
if descriptive_line:
|
||||
fragments.append(descriptive_line)
|
||||
|
||||
tag_list = self._split_keywords(nsfw_tags)
|
||||
if tag_list:
|
||||
fragments.append(f"Keywords: {', '.join(tag_list)}")
|
||||
|
||||
prompt_text = self._assemble_sentences(fragments)
|
||||
|
||||
negative_terms = []
|
||||
if include_negative_baseline:
|
||||
negative_terms.extend(NSFW_BASELINE_NEGATIVES)
|
||||
explicit_negative = self._split_keywords(explicit_cfg.get("negative", ""))
|
||||
negative_terms.extend(explicit_negative)
|
||||
heat_negative = self._split_keywords(heat_cfg.get("negative", ""))
|
||||
negative_terms.extend(heat_negative)
|
||||
negative_terms.extend(self._split_keywords(negative_prompt))
|
||||
negative_terms = list(dict.fromkeys(term.strip() for term in negative_terms if term.strip()))
|
||||
negative_text = ", ".join(negative_terms)
|
||||
|
||||
safety_parts = []
|
||||
if include_safety_note:
|
||||
safety_parts.append(NSFW_DEFAULT_SAFETY_NOTE)
|
||||
notes_sources = [
|
||||
pose_cfg.get("notes"),
|
||||
wardrobe_cfg.get("notes"),
|
||||
tone_cfg.get("notes"),
|
||||
lighting_cfg.get("notes"),
|
||||
camera_cfg.get("notes"),
|
||||
explicit_cfg.get("notes"),
|
||||
set_cfg.get("notes"),
|
||||
heat_cfg.get("notes"),
|
||||
]
|
||||
curated_notes = [note for note in notes_sources if note]
|
||||
if curated_notes:
|
||||
safety_parts.extend(list(dict.fromkeys(curated_notes)))
|
||||
if custom_safety_note.strip():
|
||||
safety_parts.append(custom_safety_note.strip())
|
||||
safety_notes = " | ".join(part for part in safety_parts if part)
|
||||
|
||||
return (prompt_text, negative_text, safety_notes, preset_status)
|
||||
|
||||
def _assemble_sentences(self, fragments):
|
||||
sentences = []
|
||||
for fragment in fragments:
|
||||
segment = fragment.strip()
|
||||
if not segment:
|
||||
continue
|
||||
if segment[-1] not in ".!?":
|
||||
segment = f"{segment}."
|
||||
sentences.append(segment)
|
||||
return " ".join(sentences)
|
||||
|
||||
def _split_keywords(self, text):
|
||||
if not text:
|
||||
return []
|
||||
separators = [",", "\n", ";"]
|
||||
tokens = [text]
|
||||
for sep in separators:
|
||||
expanded = []
|
||||
for token in tokens:
|
||||
expanded.extend(token.split(sep))
|
||||
tokens = expanded
|
||||
return [token.strip() for token in tokens if token.strip()]
|
||||
|
||||
def _resolve_nsfw_option(
|
||||
self,
|
||||
selection,
|
||||
options,
|
||||
*,
|
||||
custom_prompt="",
|
||||
custom_notes="",
|
||||
custom_negative=None,
|
||||
):
|
||||
key = self._match_option_key(selection, options)
|
||||
fallback_key = "None" if "None" in options else next(iter(options))
|
||||
source_key = key or fallback_key
|
||||
base = dict(options.get(source_key, {}))
|
||||
|
||||
prompt_parts = [part.strip() for part in [base.get("prompt", ""), custom_prompt] if part and part.strip()]
|
||||
base["prompt"] = ", ".join(dict.fromkeys(prompt_parts)) if prompt_parts else ""
|
||||
|
||||
notes_parts = [part.strip() for part in [base.get("notes", ""), custom_notes] if part and part.strip()]
|
||||
if notes_parts:
|
||||
base["notes"] = " | ".join(dict.fromkeys(notes_parts))
|
||||
elif "notes" in base:
|
||||
base["notes"] = base.get("notes", "")
|
||||
|
||||
if custom_negative is not None:
|
||||
negative_parts = [part.strip() for part in [base.get("negative", ""), custom_negative] if part and part.strip()]
|
||||
base["negative"] = ", ".join(dict.fromkeys(negative_parts)) if negative_parts else ""
|
||||
|
||||
return base
|
||||
|
||||
def _match_option_key(self, selection, options):
|
||||
normalized = (selection or "").strip()
|
||||
if not normalized:
|
||||
return None
|
||||
lookup = normalized.lower()
|
||||
for key in options.keys():
|
||||
if key.lower() == lookup:
|
||||
return key
|
||||
return None
|
||||
|
||||
__all__ = ["NoxPromptNSFWDesigner"]
|
||||
@@ -0,0 +1,117 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptPaletteMixer(PresetMixin):
|
||||
"""Generate palette overrides and keyword clusters for the builder."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mood_profile": (list(PALETTE_PROFILES.keys()), {"default": "Moody Nightfall"}),
|
||||
"intensity": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05}),
|
||||
},
|
||||
"optional": {
|
||||
"include_effects": ("BOOLEAN", {"default": True}),
|
||||
"custom_palette": ("STRING", {"multiline": True, "default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("palette_overrides", "custom_keywords", "notes", "preset_status")
|
||||
FUNCTION = "mix_palette"
|
||||
CATEGORY = "NoxPrompter/Companions"
|
||||
|
||||
def mix_palette(
|
||||
self,
|
||||
mood_profile,
|
||||
intensity,
|
||||
include_effects=True,
|
||||
custom_palette="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"mood_profile": mood_profile,
|
||||
"intensity": intensity,
|
||||
"include_effects": include_effects,
|
||||
"custom_palette": custom_palette,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"palette_mixer",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
mood_profile = config.get("mood_profile", "Moody Nightfall")
|
||||
intensity = float(config.get("intensity", 0.5))
|
||||
include_effects = bool(config.get("include_effects", True))
|
||||
custom_palette = config.get("custom_palette", "")
|
||||
|
||||
profile = PALETTE_PROFILES.get(mood_profile)
|
||||
if not profile:
|
||||
overrides_text = custom_palette.strip()
|
||||
custom_keywords = ", ".join(self._split_keywords(custom_palette))
|
||||
notes = "Custom palette applied. Profile not found."
|
||||
return (overrides_text, custom_keywords, notes, preset_status)
|
||||
|
||||
overrides = {key: list(values) for key, values in profile.get("overrides", {}).items()}
|
||||
|
||||
if intensity >= 0.6:
|
||||
for key, values in profile.get("intense_overrides", {}).items():
|
||||
overrides.setdefault(key, []).extend(values)
|
||||
|
||||
if include_effects and profile.get("effects"):
|
||||
overrides.setdefault("special_effect", []).extend(profile["effects"])
|
||||
|
||||
override_lines = []
|
||||
for key, values in overrides.items():
|
||||
deduped = list(dict.fromkeys(value for value in values if value))
|
||||
if deduped:
|
||||
override_lines.append(f"{key}: {', '.join(deduped)}")
|
||||
|
||||
if custom_palette.strip():
|
||||
override_lines.append(custom_palette.strip())
|
||||
|
||||
overrides_text = "\n".join(override_lines).strip()
|
||||
|
||||
keywords = list(profile.get("keywords", []))
|
||||
if intensity >= 0.6:
|
||||
keywords.extend(profile.get("intense_keywords", []))
|
||||
if include_effects:
|
||||
keywords.extend(profile.get("effect_keywords", []))
|
||||
if custom_palette.strip():
|
||||
keywords.extend(self._split_keywords(custom_palette))
|
||||
|
||||
keywords = list(dict.fromkeys(kw.strip() for kw in keywords if kw and kw.strip()))
|
||||
custom_keywords = ", ".join(keywords)
|
||||
|
||||
notes_parts = [profile.get("description", mood_profile)]
|
||||
notes_parts.append(f"Intensity: {intensity:.2f}")
|
||||
if include_effects and profile.get("effects"):
|
||||
notes_parts.append("Special effects included: " + ", ".join(profile["effects"]))
|
||||
if keywords:
|
||||
notes_parts.append("Keywords curated for emphasis.")
|
||||
|
||||
return (overrides_text, custom_keywords, " | ".join(notes_parts), preset_status)
|
||||
|
||||
def _split_keywords(self, text):
|
||||
if not text:
|
||||
return []
|
||||
separators = [",", "\n", ";"]
|
||||
tokens = [text]
|
||||
for sep in separators:
|
||||
temp = []
|
||||
for token in tokens:
|
||||
temp.extend(token.split(sep))
|
||||
tokens = temp
|
||||
return [token.strip() for token in tokens if token.strip()]
|
||||
|
||||
__all__ = ["NoxPromptPaletteMixer"]
|
||||
@@ -0,0 +1,392 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from ..common import detect_prompt_contradictions
|
||||
|
||||
class NoxPromptPipelineCombiner:
|
||||
"""Cascade the outputs from the Nox prompt designer suite into a master prompt."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"core_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
"enhanced_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"character_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"human_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"wardrobe_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"action_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"lighting_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"nsfw_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"narrative_subject": ("STRING", {"multiline": True, "default": ""}),
|
||||
"narrative_scene": ("STRING", {"multiline": True, "default": ""}),
|
||||
"narrative_motion": ("STRING", {"multiline": True, "default": ""}),
|
||||
"narrative_hook": ("STRING", {"multiline": True, "default": ""}),
|
||||
"palette_overrides": ("STRING", {"multiline": True, "default": ""}),
|
||||
"custom_keywords": ("STRING", {"multiline": True, "default": ""}),
|
||||
"extra_descriptors": ("STRING", {"multiline": True, "default": ""}),
|
||||
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"additional_negative": ("STRING", {"multiline": True, "default": ""}),
|
||||
"safety_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"sequence_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"section_separator": ("STRING", {"default": "\n\n"}),
|
||||
"content_separator": ("STRING", {"default": ", "}),
|
||||
"include_labels": ("BOOLEAN", {"default": True}),
|
||||
"builder_dynamic_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"action_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"lighting_notes_detail": ("STRING", {"multiline": True, "default": ""}),
|
||||
"nsfw_action_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"camz_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"camera_overrides": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("master_prompt", "negative_prompt", "pipeline_order", "reference_notes")
|
||||
FUNCTION = "assemble_pipeline"
|
||||
CATEGORY = "NoxPrompter/Workflow"
|
||||
NOTE_SPLITTER = re.compile(r"[|\n]+")
|
||||
|
||||
PIPELINE_SEQUENCE = [
|
||||
("NoxPromptCharacterCreator → define subject identity", ["character_prompt"]),
|
||||
("NoxPromptHumanDesigner → confirm adult anatomy & safeguards", ["human_prompt"]),
|
||||
("NoxPromptWardrobeDesigner → lock wardrobe & accessories", ["wardrobe_prompt"]),
|
||||
("NoxPromptNarrativeWeaver → set subject, scene, motion, hook", ["narrative_subject", "narrative_scene", "narrative_motion", "narrative_hook"]),
|
||||
("NoxPromptActionDirector / NSFW Action Director → choreograph movement", ["action_prompt"]),
|
||||
("NoxPromptLightingMaster → capture lighting & atmosphere", ["lighting_prompt"]),
|
||||
("NoxPromptNSFWDesigner → apply sensual framing & safety", ["nsfw_prompt", "safety_notes"]),
|
||||
("NoxPromptPaletteMixer → merge palette overrides & keywords", ["palette_overrides", "custom_keywords"]),
|
||||
("NoxPromptBuilder → assemble the hero prompt", ["core_prompt", "negative_prompt"]),
|
||||
("NoxPromptEnhancer → polish for delivery", ["enhanced_prompt"]),
|
||||
("NoxPromptCombiner → layer alternate blends (optional)", []),
|
||||
("NoxPromptAnalyzer → quality check (optional)", []),
|
||||
]
|
||||
|
||||
def assemble_pipeline(
|
||||
self,
|
||||
core_prompt,
|
||||
enhanced_prompt="",
|
||||
character_prompt="",
|
||||
human_prompt="",
|
||||
wardrobe_prompt="",
|
||||
action_prompt="",
|
||||
lighting_prompt="",
|
||||
nsfw_prompt="",
|
||||
narrative_subject="",
|
||||
narrative_scene="",
|
||||
narrative_motion="",
|
||||
narrative_hook="",
|
||||
palette_overrides="",
|
||||
custom_keywords="",
|
||||
extra_descriptors="",
|
||||
negative_prompt="",
|
||||
additional_negative="",
|
||||
safety_notes="",
|
||||
sequence_notes="",
|
||||
section_separator="\n\n",
|
||||
content_separator=", ",
|
||||
include_labels=True,
|
||||
builder_dynamic_notes="",
|
||||
action_notes="",
|
||||
lighting_notes_detail="",
|
||||
nsfw_action_notes="",
|
||||
camz_notes="",
|
||||
camera_overrides="",
|
||||
):
|
||||
palette_block = self._format_palette_overrides(palette_overrides)
|
||||
keywords_block = self._format_keywords(custom_keywords)
|
||||
narrative_block = self._build_narrative([narrative_subject, narrative_scene, narrative_motion, narrative_hook])
|
||||
|
||||
note_tokens = self._tokenize_notes(
|
||||
builder_dynamic_notes,
|
||||
action_notes,
|
||||
nsfw_action_notes,
|
||||
lighting_notes_detail,
|
||||
camz_notes,
|
||||
camera_overrides,
|
||||
)
|
||||
camera_tokens, motion_tokens, lighting_tokens, safety_tokens, misc_tokens = self._partition_note_tokens(note_tokens)
|
||||
|
||||
action_fragments = []
|
||||
if action_prompt.strip():
|
||||
action_fragments.append(action_prompt.strip())
|
||||
motion_clause = self._format_note_clause("Motion cues", motion_tokens, content_separator)
|
||||
if motion_clause:
|
||||
action_fragments.append(motion_clause)
|
||||
|
||||
lighting_fragments = []
|
||||
if lighting_prompt.strip():
|
||||
lighting_fragments.append(lighting_prompt.strip())
|
||||
lighting_clause = self._format_note_clause("Lighting cues", lighting_tokens, content_separator)
|
||||
if lighting_clause:
|
||||
lighting_fragments.append(lighting_clause)
|
||||
|
||||
camera_fragments = []
|
||||
camera_clause = self._format_note_clause("", camera_tokens, content_separator)
|
||||
if camera_clause:
|
||||
camera_fragments.append(camera_clause)
|
||||
|
||||
extra_fragments = []
|
||||
if extra_descriptors.strip():
|
||||
extra_fragments.append(extra_descriptors.strip())
|
||||
misc_clause = self._format_note_clause("", misc_tokens, content_separator)
|
||||
if misc_clause:
|
||||
extra_fragments.append(misc_clause)
|
||||
|
||||
safety_fragments = []
|
||||
if safety_notes.strip():
|
||||
safety_fragments.append(safety_notes.strip())
|
||||
safety_clause = self._format_note_clause("", safety_tokens, content_separator)
|
||||
if safety_clause:
|
||||
safety_fragments.append(safety_clause)
|
||||
|
||||
positive_sections = []
|
||||
|
||||
layout = [
|
||||
("Character & Identity", [character_prompt, human_prompt]),
|
||||
("Narrative Context", [narrative_block]),
|
||||
("Wardrobe & Styling", [wardrobe_prompt]),
|
||||
("Action & Motion", action_fragments),
|
||||
]
|
||||
if camera_fragments:
|
||||
layout.append(("Camera Direction", camera_fragments))
|
||||
layout.extend([
|
||||
("Lighting & Atmosphere", lighting_fragments),
|
||||
("Sensual Spotlight", [nsfw_prompt]),
|
||||
("Palette & Keywords", [palette_block, keywords_block]),
|
||||
("Extra Descriptors", extra_fragments),
|
||||
("Safety & Compliance", safety_fragments),
|
||||
("Core Prompt", [core_prompt]),
|
||||
("Enhancement Overlay", [enhanced_prompt]),
|
||||
])
|
||||
|
||||
raw_fragments_for_consistency = []
|
||||
|
||||
for label, fragments in layout:
|
||||
cleaned = [fragment.strip() for fragment in (fragments or []) if fragment and fragment.strip()]
|
||||
if not cleaned:
|
||||
continue
|
||||
section_text = content_separator.join(cleaned)
|
||||
if include_labels:
|
||||
section_text = f"{label}: {section_text}"
|
||||
positive_sections.append(section_text)
|
||||
raw_fragments_for_consistency.extend(cleaned)
|
||||
|
||||
master_prompt = section_separator.join(positive_sections).strip()
|
||||
|
||||
negative_terms = self._collect_terms([negative_prompt, additional_negative])
|
||||
aggregated_negative = ", ".join(negative_terms)
|
||||
|
||||
pipeline_lines = []
|
||||
context = {
|
||||
"core_prompt": core_prompt,
|
||||
"enhanced_prompt": enhanced_prompt,
|
||||
"character_prompt": character_prompt,
|
||||
"human_prompt": human_prompt,
|
||||
"wardrobe_prompt": wardrobe_prompt,
|
||||
"action_prompt": action_prompt,
|
||||
"lighting_prompt": lighting_prompt,
|
||||
"nsfw_prompt": nsfw_prompt,
|
||||
"narrative_subject": narrative_subject,
|
||||
"narrative_scene": narrative_scene,
|
||||
"narrative_motion": narrative_motion,
|
||||
"narrative_hook": narrative_hook,
|
||||
"palette_overrides": palette_overrides,
|
||||
"custom_keywords": custom_keywords,
|
||||
"negative_prompt": negative_prompt,
|
||||
"safety_notes": safety_notes,
|
||||
}
|
||||
|
||||
for index, (description, keys) in enumerate(self.PIPELINE_SEQUENCE, start=1):
|
||||
provided = any(context.get(key, "").strip() for key in keys) if keys else False
|
||||
status = "✓" if provided else "○"
|
||||
pipeline_lines.append(f"{index}. {status} {description}")
|
||||
|
||||
if sequence_notes.strip():
|
||||
pipeline_lines.append(f"Notes: {sequence_notes.strip()}")
|
||||
|
||||
pipeline_overview = "\n".join(pipeline_lines)
|
||||
|
||||
# Consistency analysis across assembled fragments
|
||||
consistency_warnings = detect_prompt_contradictions(
|
||||
raw_fragments_for_consistency
|
||||
+ [
|
||||
palette_overrides,
|
||||
custom_keywords,
|
||||
extra_descriptors,
|
||||
safety_notes,
|
||||
narrative_subject,
|
||||
narrative_scene,
|
||||
narrative_motion,
|
||||
narrative_hook,
|
||||
]
|
||||
)
|
||||
|
||||
reference_parts = []
|
||||
safety_reference = self._merge_lines(safety_fragments, " | ")
|
||||
if safety_reference:
|
||||
reference_parts.append(f"Safety: {safety_reference}")
|
||||
extra_palette_notes = self._collect_reference_palette(palette_overrides)
|
||||
if extra_palette_notes:
|
||||
reference_parts.append(extra_palette_notes)
|
||||
camera_reference = self._format_note_clause("", camera_tokens, ", ")
|
||||
if camera_reference:
|
||||
reference_parts.append(f"Camera: {camera_reference}")
|
||||
motion_reference = self._format_note_clause("", motion_tokens, ", ")
|
||||
if motion_reference:
|
||||
reference_parts.append(f"Motion: {motion_reference}")
|
||||
lighting_reference = self._format_note_clause("", lighting_tokens, ", ")
|
||||
if lighting_reference:
|
||||
reference_parts.append(f"Lighting: {lighting_reference}")
|
||||
descriptors_reference = self._merge_lines([
|
||||
extra_descriptors.strip(),
|
||||
self._format_note_clause("", misc_tokens, ", ")
|
||||
], " | ")
|
||||
if descriptors_reference:
|
||||
reference_parts.append(f"Descriptors: {descriptors_reference}")
|
||||
if consistency_warnings:
|
||||
reference_parts.append("Consistency: " + " | ".join(consistency_warnings))
|
||||
|
||||
reference_notes = "\n".join(reference_parts)
|
||||
|
||||
return (
|
||||
master_prompt,
|
||||
aggregated_negative,
|
||||
pipeline_overview,
|
||||
reference_notes,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _tokenize_notes(cls, *chunks):
|
||||
tokens = []
|
||||
for chunk in chunks:
|
||||
if not chunk:
|
||||
continue
|
||||
for piece in cls.NOTE_SPLITTER.split(chunk):
|
||||
stripped = piece.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
cleaned = stripped.lstrip("-•").strip()
|
||||
if cleaned:
|
||||
tokens.append(cleaned)
|
||||
return tokens
|
||||
|
||||
@staticmethod
|
||||
def _partition_note_tokens(tokens):
|
||||
camera_keywords = ("camera", "lens", "orbit", "gimbal", "steadicam", "pov", "drone", "rig", "crane", "dolly")
|
||||
motion_keywords = ("motion", "tempo", "pace", "sprint", "movement", "momentum", "flow", "beat", "action", "hover")
|
||||
lighting_keywords = ("light", "lighting", "illumination", "glow", "shadow", "highlight", "exposure", "bloom")
|
||||
safety_keywords = ("safety", "consent", "risk", "protocol", "adult", "verify", "compliance", "check-in")
|
||||
|
||||
camera_notes = []
|
||||
motion_notes = []
|
||||
lighting_notes = []
|
||||
safety_notes = []
|
||||
misc_notes = []
|
||||
|
||||
seen = set()
|
||||
for token in tokens:
|
||||
normalized = token.strip()
|
||||
if not normalized:
|
||||
continue
|
||||
key = normalized.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
if any(keyword in key for keyword in camera_keywords):
|
||||
camera_notes.append(normalized)
|
||||
elif any(keyword in key for keyword in motion_keywords):
|
||||
motion_notes.append(normalized)
|
||||
elif any(keyword in key for keyword in lighting_keywords):
|
||||
lighting_notes.append(normalized)
|
||||
elif any(keyword in key for keyword in safety_keywords):
|
||||
safety_notes.append(normalized)
|
||||
else:
|
||||
misc_notes.append(normalized)
|
||||
|
||||
return camera_notes, motion_notes, lighting_notes, safety_notes, misc_notes
|
||||
|
||||
@staticmethod
|
||||
def _format_note_clause(label, tokens, separator):
|
||||
unique = []
|
||||
seen = set()
|
||||
for token in tokens:
|
||||
cleaned = token.strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
key = cleaned.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(cleaned)
|
||||
if not unique:
|
||||
return ""
|
||||
body = separator.join(unique)
|
||||
return f"{label}: {body}" if label else body
|
||||
|
||||
@staticmethod
|
||||
def _merge_lines(fragments, separator):
|
||||
unique = []
|
||||
seen = set()
|
||||
for fragment in fragments:
|
||||
cleaned = (fragment or "").strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
key = cleaned.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(cleaned)
|
||||
return separator.join(unique)
|
||||
|
||||
@staticmethod
|
||||
def _format_palette_overrides(palette_overrides):
|
||||
content = [line.strip() for line in palette_overrides.replace("\r", "").split("\n") if line.strip()]
|
||||
return "; ".join(content)
|
||||
|
||||
@staticmethod
|
||||
def _collect_reference_palette(palette_overrides):
|
||||
content = [line.strip() for line in palette_overrides.replace("\r", "").split("\n") if line.strip()]
|
||||
if not content:
|
||||
return ""
|
||||
return "Palette: " + "; ".join(content)
|
||||
|
||||
@staticmethod
|
||||
def _format_keywords(custom_keywords):
|
||||
tokens = NoxPromptPipelineCombiner._collect_terms([custom_keywords])
|
||||
return ", ".join(tokens)
|
||||
|
||||
@staticmethod
|
||||
def _build_narrative(chunks):
|
||||
sentences = []
|
||||
for chunk in chunks:
|
||||
cleaned = (chunk or "").strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
if cleaned[-1] not in ".!?":
|
||||
cleaned = f"{cleaned}."
|
||||
sentences.append(cleaned)
|
||||
return " ".join(sentences)
|
||||
|
||||
@staticmethod
|
||||
def _collect_terms(sections):
|
||||
tokens = []
|
||||
for section in sections:
|
||||
text = (section or "").replace("\r", "\n")
|
||||
for part in text.split("\n"):
|
||||
for token in part.split(","):
|
||||
normalized = token.strip()
|
||||
if normalized:
|
||||
tokens.append(normalized)
|
||||
seen = set()
|
||||
ordered = []
|
||||
for token in tokens:
|
||||
if token.lower() in seen:
|
||||
continue
|
||||
seen.add(token.lower())
|
||||
ordered.append(token)
|
||||
return ordered
|
||||
|
||||
__all__ = ["NoxPromptPipelineCombiner"]
|
||||
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class NoxPromptOrganizer:
|
||||
"""Collect prompt fragments and present them in a consistent, readable order."""
|
||||
|
||||
CATEGORY = "NoxPrompter/Utility"
|
||||
FUNCTION = "organize"
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("organized_prompt", "missing_sections", "section_overview")
|
||||
|
||||
_STRUCTURE_MODES: Dict[str, List[str]] = {
|
||||
"Builder-first": [
|
||||
"Core Prompt",
|
||||
"Character",
|
||||
"Wardrobe",
|
||||
"Action",
|
||||
"Lighting",
|
||||
"Camera",
|
||||
"Palette",
|
||||
"Extra",
|
||||
],
|
||||
"Narrative-first": [
|
||||
"Narrative",
|
||||
"Character",
|
||||
"Action",
|
||||
"Wardrobe",
|
||||
"Lighting",
|
||||
"Camera",
|
||||
"Palette",
|
||||
"Core Prompt",
|
||||
"Extra",
|
||||
],
|
||||
"Lighting-first": [
|
||||
"Lighting",
|
||||
"Camera",
|
||||
"Action",
|
||||
"Character",
|
||||
"Wardrobe",
|
||||
"Core Prompt",
|
||||
"Palette",
|
||||
"Extra",
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"core_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
"character_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"narrative_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"wardrobe_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"action_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"lighting_prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"camera_notes": ("STRING", {"multiline": True, "default": ""}),
|
||||
"palette_overrides": ("STRING", {"multiline": True, "default": ""}),
|
||||
"extra_descriptors": ("STRING", {"multiline": True, "default": ""}),
|
||||
"structure_mode": (list(cls._STRUCTURE_MODES.keys()), {"default": "Builder-first"}),
|
||||
"include_missing_list": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
def organize(
|
||||
self,
|
||||
core_prompt: str,
|
||||
character_prompt: str = "",
|
||||
narrative_notes: str = "",
|
||||
wardrobe_prompt: str = "",
|
||||
action_prompt: str = "",
|
||||
lighting_prompt: str = "",
|
||||
camera_notes: str = "",
|
||||
palette_overrides: str = "",
|
||||
extra_descriptors: str = "",
|
||||
structure_mode: str = "Builder-first",
|
||||
include_missing_list: bool = True,
|
||||
):
|
||||
structure = self._STRUCTURE_MODES.get(structure_mode, self._STRUCTURE_MODES["Builder-first"])
|
||||
|
||||
section_values: Dict[str, str] = {
|
||||
"Core Prompt": core_prompt,
|
||||
"Narrative": narrative_notes,
|
||||
"Character": character_prompt,
|
||||
"Wardrobe": wardrobe_prompt,
|
||||
"Action": action_prompt,
|
||||
"Lighting": lighting_prompt,
|
||||
"Camera": camera_notes,
|
||||
"Palette": palette_overrides,
|
||||
"Extra": extra_descriptors,
|
||||
}
|
||||
|
||||
ordered_lines: List[str] = []
|
||||
missing: List[str] = []
|
||||
overview: List[str] = []
|
||||
|
||||
for section in structure:
|
||||
content = section_values.get(section, "").strip()
|
||||
if content:
|
||||
ordered_lines.append(f"{section}: {content}")
|
||||
overview.append(f"{section} → included")
|
||||
else:
|
||||
overview.append(f"{section} → missing")
|
||||
missing.append(section)
|
||||
|
||||
# Append any additional sections that were populated but not present in the structure definition
|
||||
for section, value in section_values.items():
|
||||
if section in structure:
|
||||
continue
|
||||
content = value.strip()
|
||||
if content:
|
||||
ordered_lines.append(f"{section}: {content}")
|
||||
overview.append(f"{section} → included (appended)")
|
||||
|
||||
organized_prompt = "\n".join(ordered_lines).strip()
|
||||
|
||||
missing_sections = ""
|
||||
if include_missing_list and missing:
|
||||
missing_sections = ", ".join(missing)
|
||||
|
||||
section_overview = "\n".join(overview).strip()
|
||||
|
||||
return organized_prompt, missing_sections, section_overview
|
||||
|
||||
|
||||
__all__ = ["NoxPromptOrganizer"]
|
||||
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
from .nox_prompt_pipeline_combiner import NoxPromptPipelineCombiner
|
||||
|
||||
|
||||
def _format_section(title: str, lines: list[str]) -> str:
|
||||
if not lines:
|
||||
return ""
|
||||
header = f"{title}\n{'-' * len(title)}"
|
||||
body = "\n".join(lines)
|
||||
return f"{header}\n{body}"
|
||||
|
||||
|
||||
class NoxPromptUsageGuide:
|
||||
"""Provide an in-canvas reference explaining how to work with the suite."""
|
||||
|
||||
CATEGORY = "NoxPrompter/Guides"
|
||||
FUNCTION = "build_guide"
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("guide_text", "pipeline_overview", "safety_highlights")
|
||||
|
||||
_DETAIL_BUNDLES = {
|
||||
"Quickstart": [
|
||||
"1. Use **NoxPromptBuilder** to assemble the base cinematic prompt.",
|
||||
"2. Layer optional specialists (Palette Mixer, Narrative Weaver, Lighting Master) to enrich the builder inputs.",
|
||||
"3. Run **NoxPromptEnhancer** for quick polish and quality tags.",
|
||||
"4. Feed everything into **NoxPromptPipelineCombiner** to generate the master prompt and negative block.",
|
||||
],
|
||||
"Extended": [
|
||||
"• Start with the Builder, but pre-seed Character Creator, Human Designer, and Wardrobe Designer to lock identity and styling.",
|
||||
"• Action Director + Lighting Master define motion and atmosphere before you finalize palette overrides.",
|
||||
"• Use Palette Mixer to create structured overrides that the Builder can ingest (copy the overrides string).",
|
||||
"• Analyzer is ideal after Enhancer to review density, key phrases, and make iteration notes.",
|
||||
],
|
||||
"Integration": [
|
||||
"• Register NODE_CLASSES from `NoxPromptNode` inside ComfyUI; all helpers are exposed there.",
|
||||
"• Preset-capable nodes (Builder, Palette Mixer, Character/Human Designer, NSFW variants) save to `custom_nodes/NoxPrompter/presets`.",
|
||||
"• Pipeline Combiner accepts raw strings, so you can splice third-party prompts or post-process with samplers easily.",
|
||||
"• Each node sticks to standard ComfyUI conventions: `INPUT_TYPES` describe exposed widgets, outputs are plain strings ready for downstream wiring.",
|
||||
],
|
||||
}
|
||||
|
||||
_TIP_LINES = [
|
||||
"• Pair Character Creator → Human Designer → Wardrobe Designer for a unified hero bible.",
|
||||
"• Palette Mixer overrides paste directly into the Builder's palette input block.",
|
||||
"• Action Director + NSFW Action Director share option tables—swap between them with the same preset names.",
|
||||
"• Lighting Master outputs both prompt text and short technical notes; send the notes into the Pipeline Combiner for reference cards.",
|
||||
"• Analyzer helps spot repeated descriptors before the final sampler prompt.",
|
||||
]
|
||||
|
||||
_SAFETY_LINES = [
|
||||
"• The NSFW Designer and NSFW Action Director automatically append baseline consent and age-negation phrases.",
|
||||
"• Leave `include_safety_note` enabled in NSFW nodes unless you replace it with a studio policy alternative.",
|
||||
"• Palette Mixer + Builder share override keys; keep them consistent so lighting or wardrobe selections do not fight each other.",
|
||||
"• Final pass: run Pipeline Combiner and scan the Safety & Compliance section before triggering renders.",
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"detail_level": (list(cls._DETAIL_BUNDLES.keys()), {"default": "Quickstart"}),
|
||||
},
|
||||
"optional": {
|
||||
"include_pipeline": ("BOOLEAN", {"default": True}),
|
||||
"include_safety": ("BOOLEAN", {"default": True}),
|
||||
"include_tips": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, *_args, **_kwargs): # pragma: no cover - ComfyUI metadata hook
|
||||
return True
|
||||
|
||||
def build_guide(
|
||||
self,
|
||||
detail_level: str,
|
||||
include_pipeline: bool = True,
|
||||
include_safety: bool = True,
|
||||
include_tips: bool = True,
|
||||
):
|
||||
detail_key = detail_level if detail_level in self._DETAIL_BUNDLES else "Quickstart"
|
||||
intro = _format_section(
|
||||
"Nox Prompter Overview",
|
||||
[
|
||||
"The custom node suite is modular: start from the builder, layer specialists,"
|
||||
" and finish with the Pipeline Combiner for a structured master prompt.",
|
||||
],
|
||||
)
|
||||
|
||||
detail_section = _format_section(
|
||||
f"{detail_key} Workflow",
|
||||
self._DETAIL_BUNDLES.get(detail_key, []),
|
||||
)
|
||||
|
||||
tip_section = ""
|
||||
if include_tips:
|
||||
tip_section = _format_section("Suggested Pairings", self._TIP_LINES)
|
||||
|
||||
guide_parts = [segment for segment in (intro, detail_section, tip_section) if segment]
|
||||
guide_text = "\n\n".join(guide_parts)
|
||||
|
||||
pipeline_text = ""
|
||||
if include_pipeline:
|
||||
lines = []
|
||||
for index, (description, _keys) in enumerate(NoxPromptPipelineCombiner.PIPELINE_SEQUENCE, start=1):
|
||||
token = f"{index}. {description}"
|
||||
lines.append(token)
|
||||
pipeline_text = dedent(
|
||||
"\n".join([
|
||||
"Recommended Pipeline",
|
||||
"--------------------",
|
||||
*lines,
|
||||
])
|
||||
).strip()
|
||||
|
||||
safety_text = ""
|
||||
if include_safety:
|
||||
safety_text = _format_section("Safety & Consistency", self._SAFETY_LINES)
|
||||
|
||||
return (guide_text, pipeline_text, safety_text)
|
||||
|
||||
|
||||
__all__ = ["NoxPromptUsageGuide"]
|
||||
@@ -0,0 +1,217 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Tuple
|
||||
from ..common import PresetMixin, _resolve_with_custom, _split_tokens, _format_notes
|
||||
from ..constants import * # noqa: F403
|
||||
|
||||
class NoxPromptWardrobeDesigner(PresetMixin):
|
||||
"""Generate advanced wardrobe breakdowns with styling guidance."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"base_garment": (list(WARDROBE_BASE_GARMENT_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Battle Dress"}),
|
||||
"silhouette": (list(WARDROBE_SILHOUETTE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "A-Line"}),
|
||||
"colorway": (list(WARDROBE_COLORWAY_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Monochrome Obsidian"}),
|
||||
},
|
||||
"optional": {
|
||||
"layering": (list(WARDROBE_LAYER_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Armor Plating"}),
|
||||
"fabric": (list(WARDROBE_FABRIC_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Ballistic Weave"}),
|
||||
"pattern": (list(WARDROBE_PATTERN_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Fractal"}),
|
||||
"wear_state": (list(WARDROBE_WEAR_STATE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Battle-Worn"}),
|
||||
"culture_inspiration": (list(WARDROBE_CULTURE_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Solar Court"}),
|
||||
"footwear": (list(WARDROBE_FOOTWEAR_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Combat Boots"}),
|
||||
"primary_accessory": (list(WARDROBE_ACCESSORY_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Relic Pendant"}),
|
||||
"secondary_accessory": (list(WARDROBE_ACCESSORY_OPTIONS.keys()) + [CUSTOM_OPTION], {"default": "Utility Belt"}),
|
||||
"functional_focus": ("STRING", {"default": ""}),
|
||||
"accent_materials": ("STRING", {"multiline": True, "default": ""}),
|
||||
"motion_considerations": ("STRING", {"multiline": True, "default": ""}),
|
||||
"finishing_details": ("STRING", {"multiline": True, "default": ""}),
|
||||
"additional_accessories": ("STRING", {"multiline": True, "default": ""}),
|
||||
"base_garment_custom": ("STRING", {"default": ""}),
|
||||
"silhouette_custom": ("STRING", {"default": ""}),
|
||||
"colorway_custom": ("STRING", {"default": ""}),
|
||||
"layering_custom": ("STRING", {"default": ""}),
|
||||
"fabric_custom": ("STRING", {"default": ""}),
|
||||
"pattern_custom": ("STRING", {"default": ""}),
|
||||
"wear_state_custom": ("STRING", {"default": ""}),
|
||||
"culture_inspiration_custom": ("STRING", {"default": ""}),
|
||||
"footwear_custom": ("STRING", {"default": ""}),
|
||||
"primary_accessory_custom": ("STRING", {"default": ""}),
|
||||
"secondary_accessory_custom": ("STRING", {"default": ""}),
|
||||
"preset_action": (["none", "save", "load", "list"], {"default": "none"}),
|
||||
"preset_name": ("STRING", {"default": ""}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
||||
RETURN_NAMES = ("wardrobe_prompt", "styling_notes", "accessory_list", "preset_status")
|
||||
FUNCTION = "design"
|
||||
CATEGORY = "NoxPrompter/Wardrobe"
|
||||
|
||||
def design(
|
||||
self,
|
||||
base_garment,
|
||||
silhouette,
|
||||
colorway,
|
||||
layering="Armor Plating",
|
||||
fabric="Ballistic Weave",
|
||||
pattern="Fractal",
|
||||
wear_state="Battle-Worn",
|
||||
culture_inspiration="Solar Court",
|
||||
footwear="Combat Boots",
|
||||
primary_accessory="Relic Pendant",
|
||||
secondary_accessory="Utility Belt",
|
||||
functional_focus="",
|
||||
accent_materials="",
|
||||
motion_considerations="",
|
||||
finishing_details="",
|
||||
additional_accessories="",
|
||||
base_garment_custom="",
|
||||
silhouette_custom="",
|
||||
colorway_custom="",
|
||||
layering_custom="",
|
||||
fabric_custom="",
|
||||
pattern_custom="",
|
||||
wear_state_custom="",
|
||||
culture_inspiration_custom="",
|
||||
footwear_custom="",
|
||||
primary_accessory_custom="",
|
||||
secondary_accessory_custom="",
|
||||
preset_action="none",
|
||||
preset_name="",
|
||||
):
|
||||
config = {
|
||||
"base_garment": base_garment,
|
||||
"silhouette": silhouette,
|
||||
"colorway": colorway,
|
||||
"layering": layering,
|
||||
"fabric": fabric,
|
||||
"pattern": pattern,
|
||||
"wear_state": wear_state,
|
||||
"culture_inspiration": culture_inspiration,
|
||||
"footwear": footwear,
|
||||
"primary_accessory": primary_accessory,
|
||||
"secondary_accessory": secondary_accessory,
|
||||
"functional_focus": functional_focus,
|
||||
"accent_materials": accent_materials,
|
||||
"motion_considerations": motion_considerations,
|
||||
"finishing_details": finishing_details,
|
||||
"additional_accessories": additional_accessories,
|
||||
"base_garment_custom": base_garment_custom,
|
||||
"silhouette_custom": silhouette_custom,
|
||||
"colorway_custom": colorway_custom,
|
||||
"layering_custom": layering_custom,
|
||||
"fabric_custom": fabric_custom,
|
||||
"pattern_custom": pattern_custom,
|
||||
"wear_state_custom": wear_state_custom,
|
||||
"culture_inspiration_custom": culture_inspiration_custom,
|
||||
"footwear_custom": footwear_custom,
|
||||
"primary_accessory_custom": primary_accessory_custom,
|
||||
"secondary_accessory_custom": secondary_accessory_custom,
|
||||
}
|
||||
|
||||
config, preset_status = self._apply_preset_action(
|
||||
"wardrobe_designer",
|
||||
preset_action,
|
||||
preset_name,
|
||||
config,
|
||||
)
|
||||
|
||||
base_garment = config.get("base_garment", base_garment)
|
||||
silhouette = config.get("silhouette", silhouette)
|
||||
colorway = config.get("colorway", colorway)
|
||||
layering = config.get("layering", layering)
|
||||
fabric = config.get("fabric", fabric)
|
||||
pattern = config.get("pattern", pattern)
|
||||
wear_state = config.get("wear_state", wear_state)
|
||||
culture_inspiration = config.get("culture_inspiration", culture_inspiration)
|
||||
footwear = config.get("footwear", footwear)
|
||||
primary_accessory = config.get("primary_accessory", primary_accessory)
|
||||
secondary_accessory = config.get("secondary_accessory", secondary_accessory)
|
||||
functional_focus = config.get("functional_focus", functional_focus)
|
||||
accent_materials = config.get("accent_materials", accent_materials)
|
||||
motion_considerations = config.get("motion_considerations", motion_considerations)
|
||||
finishing_details = config.get("finishing_details", finishing_details)
|
||||
additional_accessories = config.get("additional_accessories", additional_accessories)
|
||||
base_garment_custom = config.get("base_garment_custom", base_garment_custom)
|
||||
silhouette_custom = config.get("silhouette_custom", silhouette_custom)
|
||||
colorway_custom = config.get("colorway_custom", colorway_custom)
|
||||
layering_custom = config.get("layering_custom", layering_custom)
|
||||
fabric_custom = config.get("fabric_custom", fabric_custom)
|
||||
pattern_custom = config.get("pattern_custom", pattern_custom)
|
||||
wear_state_custom = config.get("wear_state_custom", wear_state_custom)
|
||||
culture_inspiration_custom = config.get("culture_inspiration_custom", culture_inspiration_custom)
|
||||
footwear_custom = config.get("footwear_custom", footwear_custom)
|
||||
primary_accessory_custom = config.get("primary_accessory_custom", primary_accessory_custom)
|
||||
secondary_accessory_custom = config.get("secondary_accessory_custom", secondary_accessory_custom)
|
||||
|
||||
base_prompt, base_notes = _resolve_with_custom(base_garment, base_garment_custom, WARDROBE_BASE_GARMENT_OPTIONS)
|
||||
silhouette_prompt, silhouette_notes = _resolve_with_custom(silhouette, silhouette_custom, WARDROBE_SILHOUETTE_OPTIONS)
|
||||
color_prompt, color_notes = _resolve_with_custom(colorway, colorway_custom, WARDROBE_COLORWAY_OPTIONS)
|
||||
layering_prompt, layering_notes = _resolve_with_custom(layering, layering_custom, WARDROBE_LAYER_OPTIONS)
|
||||
fabric_prompt, fabric_notes = _resolve_with_custom(fabric, fabric_custom, WARDROBE_FABRIC_OPTIONS)
|
||||
pattern_prompt, pattern_notes = _resolve_with_custom(pattern, pattern_custom, WARDROBE_PATTERN_OPTIONS)
|
||||
wear_prompt, wear_notes = _resolve_with_custom(wear_state, wear_state_custom, WARDROBE_WEAR_STATE_OPTIONS)
|
||||
culture_prompt, culture_notes = _resolve_with_custom(culture_inspiration, culture_inspiration_custom, WARDROBE_CULTURE_OPTIONS)
|
||||
footwear_prompt, footwear_notes = _resolve_with_custom(footwear, footwear_custom, WARDROBE_FOOTWEAR_OPTIONS)
|
||||
primary_prompt, primary_notes = _resolve_with_custom(primary_accessory, primary_accessory_custom, WARDROBE_ACCESSORY_OPTIONS)
|
||||
secondary_prompt, secondary_notes = _resolve_with_custom(secondary_accessory, secondary_accessory_custom, WARDROBE_ACCESSORY_OPTIONS)
|
||||
|
||||
accent_list = _split_tokens(accent_materials)
|
||||
motion_list = _split_tokens(motion_considerations)
|
||||
finishing_list = _split_tokens(finishing_details)
|
||||
extra_accessories = _split_tokens(additional_accessories)
|
||||
|
||||
wardrobe_prompt_parts = [
|
||||
base_prompt,
|
||||
silhouette_prompt,
|
||||
fabric_prompt,
|
||||
layering_prompt,
|
||||
color_prompt,
|
||||
pattern_prompt,
|
||||
wear_prompt,
|
||||
culture_prompt,
|
||||
footwear_prompt,
|
||||
]
|
||||
if accent_list:
|
||||
wardrobe_prompt_parts.append("accents: " + ", ".join(accent_list))
|
||||
if finishing_list:
|
||||
wardrobe_prompt_parts.append("finishing: " + ", ".join(finishing_list))
|
||||
|
||||
wardrobe_prompt = ", ".join(part for part in wardrobe_prompt_parts if part).strip()
|
||||
if wardrobe_prompt and wardrobe_prompt[-1] not in ".!?":
|
||||
wardrobe_prompt += "."
|
||||
if functional_focus.strip():
|
||||
wardrobe_prompt += f" Functional focus: {functional_focus.strip()}."
|
||||
if motion_list:
|
||||
wardrobe_prompt += f" Movement considerations: {', '.join(motion_list)}."
|
||||
|
||||
styling_segments = [
|
||||
_format_notes("Base", base_notes, base_prompt),
|
||||
_format_notes("Silhouette", silhouette_notes, silhouette_prompt),
|
||||
_format_notes("Fabric", fabric_notes, fabric_prompt),
|
||||
_format_notes("Layering", layering_notes, layering_prompt),
|
||||
_format_notes("Colorway", color_notes, color_prompt),
|
||||
_format_notes("Pattern", pattern_notes, pattern_prompt),
|
||||
_format_notes("Wear State", wear_notes, wear_prompt),
|
||||
_format_notes("Culture", culture_notes, culture_prompt),
|
||||
_format_notes("Footwear", footwear_notes, footwear_prompt),
|
||||
]
|
||||
styling_notes = " | ".join(segment for segment in styling_segments if segment)
|
||||
|
||||
accessory_items = [
|
||||
primary_prompt,
|
||||
secondary_prompt if secondary_accessory else "",
|
||||
]
|
||||
accessory_notes = [note for note in [primary_notes, secondary_notes] if note]
|
||||
if extra_accessories:
|
||||
accessory_items.append("Additional: " + ", ".join(extra_accessories))
|
||||
accessory_list = " | ".join(item for item in accessory_items if item)
|
||||
if accessory_notes:
|
||||
accessory_list += (" | Notes: " + "; ".join(accessory_notes)) if accessory_list else "Notes: " + "; ".join(accessory_notes)
|
||||
|
||||
return (wardrobe_prompt, styling_notes, accessory_list, preset_status)
|
||||
|
||||
__all__ = ["NoxPromptWardrobeDesigner"]
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"formula": "Advanced Formula",
|
||||
"subject_focus": "A curious farm girl hero",
|
||||
"scene_setting": "Sunlit meadow with tall grass swaying in the breeze",
|
||||
"motion_arc": "Dashes forward laughing, arms outstretched to touch the flowers",
|
||||
"narrative_hook": "Camera orbits as she reaches toward the horizon",
|
||||
"model_emphasis": "Precise semantic adherence, cinematic aesthetic control",
|
||||
"light_source": "Daylight",
|
||||
"light_quality": "Soft Light",
|
||||
"time_of_day": "Daytime",
|
||||
"shot_size": "Medium Close-up",
|
||||
"composition": "Balanced",
|
||||
"lens_focal": "Medium",
|
||||
"lens_angle": "Low Angle",
|
||||
"lens_type": "Single Shot",
|
||||
"color_tone": "Warm Tone",
|
||||
"motion_type": "Running",
|
||||
"emotion": "Joy",
|
||||
"camera_basic": "Pan Right",
|
||||
"camera_advanced": "Following",
|
||||
"visual_style": "Anime",
|
||||
"special_effect": "Tilt-shift",
|
||||
"keyword_style": "inline",
|
||||
"quality_profile": "Balanced",
|
||||
"randomize_missing": true,
|
||||
"random_seed": 7,
|
||||
"palette_overrides": "light_source: Moonlight\ncolor_tone: Cool Tone",
|
||||
"extra_descriptors": "Shot on 85mm lens, 16fps preview",
|
||||
"custom_keywords": "heroic energy, epic storytelling",
|
||||
"negative_prompt": "blurry, low detail",
|
||||
"prompt_prefix": "Wan 2.2 cinematic prompt",
|
||||
"prompt_suffix": "Render ready for upscale",
|
||||
"prompt_filter_enabled": true,
|
||||
"prompt_filter_profile": "balanced"
|
||||
}
|
||||
Reference in New Issue
Block a user