update To V2

This commit is contained in:
James Knox
2025-10-01 17:45:46 -05:00
parent 10e58dde81
commit b786d9bd3f
68 changed files with 8456 additions and 2063 deletions
+70 -2063
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -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",
}
+31
View File
@@ -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.
+325
View File
@@ -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
+75
View File
@@ -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",
]
@@ -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"]
+130
View File
@@ -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"]
+444
View File
@@ -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"]
+219
View File
@@ -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"]
+119
View File
@@ -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"]
+190
View File
@@ -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"]
+116
View File
@@ -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"]
+126
View File
@@ -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"
}