Refine Scene Builder editor
This commit is contained in:
+3
-1
@@ -8,7 +8,9 @@ Added `FunPack Scene Builder`, a scene preset node that replaces `FunPack Templa
|
||||
|
||||
Simplified `FunPack Scene Builder` so prompt and intent text are connection-only inputs, removed model-mode and per-block controls, and outputs only scene prompt data plus the pass-through FunPack LoRA stack instead of conditioning.
|
||||
|
||||
Added `Learning` mode to `FunPack Scene Builder`; it records connected prompt phrases into the selected refinement key's scene memory while passing positive prompt, negative prompt, sigmas, refinement key, and LoRA stack through unchanged. Refiner reset clears conditioning-delta learning while preserving the refinement key's Scene Builder memory.
|
||||
Added `Learning` mode to `FunPack Scene Builder`; it records connected prompt phrases into the selected refinement key's scene memory while passing positive prompt, negative prompt, and LoRA stack through unchanged. Refiner reset clears conditioning-delta learning while preserving the refinement key's Scene Builder memory.
|
||||
|
||||
Redesigned `FunPack Scene Builder` as a compact editor with visible scene name/mode controls, Positive Prompt and Negative Prompt text composers, a Database menu for phrase/category/wildcard editing, and only prompt/LoRA stack/status outputs.
|
||||
|
||||
Added searchable LoRA picking to `FunPack Apply LoRA Weights`. The compact row UI remains the primary workflow, and saved workflows still serialize through the existing `lora_list` JSON value.
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ A set of ComfyUI nodes for experimenting with video generation workflows based o
|
||||
|
||||
## Updates in 2.3.4
|
||||
|
||||
Added `FunPack Scene Builder`, a replacement for `FunPack Template Manager`. It stores named scenes from manually selected universal prompt phrases, takes prompt/intent text through connection-only inputs, passes the current LoRA stack through unchanged, and can auto-apply a scene when its name or alias appears in an intent prompt.
|
||||
Added `FunPack Scene Builder`, a replacement for `FunPack Template Manager`. It stores named scenes from editable positive/negative prompt text, takes prompt/intent text through connection-only inputs, passes the current LoRA stack through unchanged, and can auto-apply a scene when its name or alias appears in an intent prompt.
|
||||
|
||||
Added Scene Builder `Learning` mode for changing-prompt generations: it collects phrase memory inside the selected refinement key while passing the connected prompt data through unchanged, and Refiner resets preserve that Scene Builder memory.
|
||||
|
||||
Redesigned Scene Builder as a compact editor with visible scene name/mode controls plus Positive Prompt, Negative Prompt, and Database menus. Prompt editors preserve freely typed text while phrase chips can be clicked or dragged in from the database.
|
||||
|
||||
Added searchable LoRA selection to `FunPack Apply LoRA Weights` while keeping the compact row UI and serialized LoRA stack format.
|
||||
|
||||
Added advisory image/CLIP Vision context and repaired negative conditioning to `FunPack Video Refiner V2`. Vision inputs are stored as metadata and diagnostics only; they are not blended into positive conditioning.
|
||||
|
||||
+35
-29
@@ -1,32 +1,40 @@
|
||||
# FunPack Scene Builder
|
||||
|
||||
`FunPack Scene Builder` stores named scene presets inside the selected refinement key, separate from Refiner's conditioning-delta learning. Connected positive and negative prompts are memory sources only: every queue run collects their phrases into universal memory, but the node outputs only the selected scene phrases, a saved scene preset, or pass-through text in Learning mode.
|
||||
`FunPack Scene Builder` stores named scene presets inside the selected refinement key, separate from Refiner's conditioning-delta learning. Connected positive and negative prompts are memory sources only: every queue run collects their phrases into universal memory, but the node outputs composed scene text or pass-through text in Learning mode.
|
||||
|
||||
## Visible Controls
|
||||
|
||||
**scene_name**: The scene being edited or saved.
|
||||
|
||||
**mode**: `Manual`, `Auto`, or `Learning`.
|
||||
|
||||
All other Scene Builder controls live in the custom editor UI.
|
||||
|
||||
## Editor Menus
|
||||
|
||||
**Positive prompt**: Opens a multiline text composer. Typed text, commas, periods, and manual ordering are preserved exactly. Database chips can be clicked or dragged into the text field.
|
||||
|
||||
**Negative prompt**: Same composer behavior for the negative prompt.
|
||||
|
||||
**Database**: Add, edit, delete, search, categorize, and wildcard-group universal words and phrases.
|
||||
|
||||
Each menu has **Back**, **Cancel**, and **Confirm** controls. Cancel restores the menu snapshot from before editing. Confirm saves prompt/database edits.
|
||||
|
||||
## Modes
|
||||
|
||||
**Manual** outputs the current Scene Builder GUI selection: selected positive phrases, selected negative phrases, sigmas, refinement key, and the connected LoRA stack passed through unchanged.
|
||||
**Manual** outputs the current composed positive and negative scene text, plus the connected LoRA stack passed through unchanged.
|
||||
|
||||
**Auto** scans `intent_prompt` for a saved scene name or alias. Exact matches are preferred, then a conservative word-match fallback is used. If nothing matches, the node falls back to Manual output.
|
||||
|
||||
**Learning** saves connected positive and negative prompt phrases into Scene Builder universal memory, then passes the connected positive prompt, negative prompt, sigmas, refinement key, and LoRA stack through unchanged.
|
||||
**Learning** saves connected positive and negative prompt phrases into Scene Builder universal memory, then passes the connected positive prompt, negative prompt, and LoRA stack through unchanged.
|
||||
|
||||
## Inputs
|
||||
## Connection-Only Inputs
|
||||
|
||||
**scene**: Saved scene dropdown.
|
||||
**intent_prompt**: Text used for Auto scene detection. This can be the same intent prompt that feeds Refiner V2.
|
||||
|
||||
**scene_name**: Name used when saving a scene.
|
||||
**positive_prompt** and **negative_prompt**: Universal memory sources. They do not override scene output except in Learning pass-through mode.
|
||||
|
||||
**aliases**: Comma-separated trigger names for Auto mode.
|
||||
|
||||
**output_mode**: `Manual`, `Auto`, or `Learning`.
|
||||
|
||||
**intent_prompt**: Connection-only text used for Auto scene detection. This can be the same intent prompt that feeds Refiner V2.
|
||||
|
||||
**positive_prompt** and **negative_prompt**: Connection-only universal memory sources. They do not override scene output and do not appear as editable text fields on the node.
|
||||
|
||||
**refinement_key** / **refinement_key_input**: Key that owns this Scene Builder memory and saved scenes. The value is also passed through with the scene.
|
||||
|
||||
**sigmas**: Optional sigma schedule stored with saved scenes.
|
||||
**refinement_key_input**: Optional key that owns this Scene Builder memory and saved scenes.
|
||||
|
||||
**lora_stack**: Optional current LoRA stack. Scene Builder passes it through unchanged so Refiner can use the active LoRAs for suggestions.
|
||||
|
||||
@@ -34,25 +42,23 @@ Scene Builder memory is stored in the selected refinement key under its own `sce
|
||||
|
||||
## Outputs
|
||||
|
||||
**positive_prompt**: Selected positive scene phrases joined with commas.
|
||||
**positive_prompt**: Composed positive scene text.
|
||||
|
||||
**negative_prompt**: Selected negative scene phrases joined with commas.
|
||||
|
||||
**scene_name**: The manual scene name or the Auto-matched saved scene.
|
||||
|
||||
**sigmas**: Stored scene sigmas, current connected sigmas, or an empty sigma tensor.
|
||||
**negative_prompt**: Composed negative scene text.
|
||||
|
||||
**lora_stack**: The connected `FUNPACK_LORA_STACK`, passed through unchanged.
|
||||
|
||||
**refinement_key**: The scene refinement key.
|
||||
|
||||
**status**: Summary of selected mode, matched scene, phrase counts, and pass-through LoRA stack count.
|
||||
|
||||
## Wildcards
|
||||
|
||||
Database phrases can share a wildcard group. If multiple phrases from the same wildcard group appear in the runtime composed prompt, Scene Builder outputs one randomly selected phrase from that group and omits the others. The saved prompt text is not rewritten.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Connect positive and negative prompt text, then queue once to collect phrase memory.
|
||||
2. Select phrase chips from the universal phrase bank.
|
||||
3. Connect the current LoRA stack if Refiner should receive active LoRAs for suggestions.
|
||||
4. Enter a scene name and press **Save**.
|
||||
5. Switch to **Auto** and write a saved scene name or alias into `intent_prompt` to apply that preset automatically.
|
||||
2. Open **Database** to clean up phrases, assign categories, or set wildcard groups.
|
||||
3. Open **Positive prompt** or **Negative prompt** to type freely and insert chips from the database.
|
||||
4. Enter a scene name and confirm or save the scene.
|
||||
5. Use **Auto** with a scene name or alias in `intent_prompt` to apply a saved scene automatically.
|
||||
6. Use **Learning** when generating changing prompts and you want Scene Builder to collect memory while leaving the generation prompt untouched.
|
||||
|
||||
+220
-66
@@ -324,7 +324,7 @@ def categorize_scene_phrase(text, source="positive"):
|
||||
def extract_scene_phrases(text, source="positive"):
|
||||
phrases = []
|
||||
seen = set()
|
||||
for raw in re.split(r"[,;\n]+", str(text or "")):
|
||||
for raw in re.split(r"[,;.\n]+", str(text or "")):
|
||||
clean = re.sub(r"\s+", " ", raw).strip(" ,;:.").strip()
|
||||
if len(clean) < 2:
|
||||
continue
|
||||
@@ -385,10 +385,47 @@ def scene_memory_items(data):
|
||||
"category": item.get("category", "details"),
|
||||
"tokens": item.get("tokens", scene_token_words(text)),
|
||||
"count": int(item.get("count", 0) or 0),
|
||||
"wildcard_group": str(item.get("wildcard_group") or "").strip(),
|
||||
})
|
||||
return sorted(items, key=lambda item: (item["category"], item["text"].lower()))
|
||||
|
||||
|
||||
def normalize_scene_memory_items(value):
|
||||
if isinstance(value, dict):
|
||||
iterable = value.items()
|
||||
elif isinstance(value, list):
|
||||
iterable = ((None, item) for item in value)
|
||||
else:
|
||||
return {}
|
||||
|
||||
memory = {}
|
||||
for fallback_key, item in iterable:
|
||||
if isinstance(item, str):
|
||||
item = {"text": item}
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
text = re.sub(r"\s+", " ", str(item.get("text") or fallback_key or "").strip())
|
||||
if not text:
|
||||
continue
|
||||
key = normalize_scene_key(item.get("key") or text) or text.lower()
|
||||
source = "negative" if item.get("source") == "negative" or item.get("category") == "negative" else "positive"
|
||||
category = str(item.get("category") or categorize_scene_phrase(text, source)).strip().lower()
|
||||
if category not in SCENE_CATEGORIES:
|
||||
category = "negative" if source == "negative" else "details"
|
||||
tokens = scene_token_words(text)
|
||||
memory[key] = {
|
||||
"text": text,
|
||||
"source": source,
|
||||
"category": category,
|
||||
"tokens": tokens,
|
||||
"count": max(0, int(item.get("count", 0) or 0)),
|
||||
"created_at": str(item.get("created_at") or now_iso()),
|
||||
"updated_at": now_iso(),
|
||||
"wildcard_group": str(item.get("wildcard_group") or "").strip(),
|
||||
}
|
||||
return memory
|
||||
|
||||
|
||||
def normalize_scene_phrase_list(value):
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
@@ -428,6 +465,13 @@ def parse_scene_payload(value):
|
||||
}
|
||||
|
||||
|
||||
def scene_text_or_payload(text, phrases):
|
||||
text = str(text or "").strip()
|
||||
if text:
|
||||
return text
|
||||
return scene_text_from_phrases(phrases)
|
||||
|
||||
|
||||
def scene_payload_from_scene(scene):
|
||||
if not isinstance(scene, dict):
|
||||
return {"positive_phrases": [], "negative_phrases": []}
|
||||
@@ -441,10 +485,29 @@ def scene_text_from_phrases(phrases):
|
||||
return ", ".join(normalize_scene_phrase_list(phrases))
|
||||
|
||||
|
||||
def scene_from_texts(name, aliases, output_mode, positive_text, negative_text, refinement_key=""):
|
||||
positive_text = str(positive_text or "").strip()
|
||||
negative_text = str(negative_text or "").strip()
|
||||
return {
|
||||
"name": normalize_scene_name(name),
|
||||
"aliases": aliases_from_text(aliases),
|
||||
"output_mode": output_mode if output_mode in {"Manual", "Auto", "Learning"} else "Manual",
|
||||
"refinement_key": normalize_refinement_key(refinement_key),
|
||||
"positive_text": positive_text,
|
||||
"negative_text": negative_text,
|
||||
"positive_phrases": normalize_scene_phrase_list(extract_scene_phrases(positive_text, "positive")),
|
||||
"negative_phrases": normalize_scene_phrase_list(extract_scene_phrases(negative_text, "negative")),
|
||||
}
|
||||
|
||||
|
||||
def aliases_from_text(value):
|
||||
aliases = []
|
||||
seen = set()
|
||||
for raw in re.split(r"[,;\n]+", str(value or "")):
|
||||
if isinstance(value, list):
|
||||
raw_values = value
|
||||
else:
|
||||
raw_values = re.split(r"[,;\n]+", str(value or ""))
|
||||
for raw in raw_values:
|
||||
clean = re.sub(r"\s+", " ", raw).strip()
|
||||
key = normalize_scene_key(clean)
|
||||
if clean and key not in seen:
|
||||
@@ -505,9 +568,13 @@ def scene_field_summary(scene):
|
||||
if not isinstance(scene, dict):
|
||||
return "none"
|
||||
fields = []
|
||||
for field in ("positive_phrases", "negative_phrases", "refinement_key", "sigmas"):
|
||||
for field in ("positive_phrases", "negative_phrases", "refinement_key"):
|
||||
if scene.get(field):
|
||||
fields.append(field)
|
||||
if scene.get("positive_text"):
|
||||
fields.append("positive_text")
|
||||
if scene.get("negative_text"):
|
||||
fields.append("negative_text")
|
||||
return ", ".join(fields) if fields else "none"
|
||||
|
||||
|
||||
@@ -589,6 +656,56 @@ def resolve_wildcards(text, seed=0):
|
||||
return current
|
||||
|
||||
|
||||
def resolve_scene_database_wildcards(text, scene_db):
|
||||
text = str(text or "")
|
||||
if not text or not isinstance(scene_db, dict):
|
||||
return text
|
||||
|
||||
groups = {}
|
||||
for item in scene_db.get("universal_memory", {}).values():
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
group = str(item.get("wildcard_group") or "").strip()
|
||||
phrase = str(item.get("text") or "").strip()
|
||||
if not group or not phrase:
|
||||
continue
|
||||
groups.setdefault(group, {})[normalize_scene_key(phrase)] = phrase
|
||||
|
||||
if not groups:
|
||||
return text
|
||||
|
||||
parts = re.split(r"([,;.\n]+)", text)
|
||||
matches_by_group = {}
|
||||
for index in range(0, len(parts), 2):
|
||||
phrase_key = normalize_scene_key(parts[index])
|
||||
if not phrase_key:
|
||||
continue
|
||||
for group, phrases in groups.items():
|
||||
if phrase_key in phrases:
|
||||
matches_by_group.setdefault(group, []).append((index, phrase_key))
|
||||
break
|
||||
|
||||
changed = False
|
||||
for matches in matches_by_group.values():
|
||||
unique_keys = sorted({phrase_key for _, phrase_key in matches})
|
||||
if len(unique_keys) < 2:
|
||||
continue
|
||||
keep = random.choice(unique_keys)
|
||||
for index, phrase_key in matches:
|
||||
if phrase_key != keep:
|
||||
parts[index] = ""
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
return text
|
||||
|
||||
output = "".join(parts)
|
||||
output = re.sub(r"\s*([,;.])\s*([,;.]\s*)+", r"\1 ", output)
|
||||
output = re.sub(r"(?:^|[\s,;.])+\n", "\n", output)
|
||||
output = re.sub(r"\s+", " ", output.replace("\n", ", ")).strip(" ,;.")
|
||||
return output
|
||||
|
||||
|
||||
def prompt_key_for_refiner(prompt, mode):
|
||||
del mode
|
||||
return re.sub(r"\s+", " ", str(prompt or "").strip())
|
||||
@@ -773,6 +890,56 @@ async def funpack_scenes_export(request):
|
||||
)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/funpack/scenes/scene")
|
||||
async def funpack_scene_save(request):
|
||||
key = normalize_refinement_key(request.query.get("key", ""))
|
||||
body = await request.json()
|
||||
if not isinstance(body, dict):
|
||||
return web.json_response({"error": "Scene payload must be an object."}, status=400)
|
||||
|
||||
action = str(body.get("action") or "save").lower()
|
||||
name = normalize_scene_name(body.get("name"))
|
||||
if not name:
|
||||
return web.json_response({"error": "Scene name is required."}, status=400)
|
||||
|
||||
data = load_scene_db(key)
|
||||
scenes = data.setdefault("scenes", {})
|
||||
if action == "delete":
|
||||
scenes.pop(name, None)
|
||||
save_scene_db(data, key)
|
||||
return web.json_response({"deleted": name, "scenes": scene_names(key), "data": data})
|
||||
|
||||
previous = scenes.get(name, {}) if isinstance(scenes.get(name), dict) else {}
|
||||
scene = scene_from_texts(
|
||||
name,
|
||||
body.get("aliases", previous.get("aliases", [])),
|
||||
body.get("mode") or body.get("output_mode") or previous.get("output_mode", "Manual"),
|
||||
body.get("positive_text", previous.get("positive_text", "")),
|
||||
body.get("negative_text", previous.get("negative_text", "")),
|
||||
key,
|
||||
)
|
||||
scene["created_at"] = previous.get("created_at", now_iso())
|
||||
scene["updated_at"] = now_iso()
|
||||
scenes[name] = scene
|
||||
save_scene_db(data, key)
|
||||
return web.json_response({"saved": name, "scenes": scene_names(key), "data": data})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/funpack/scenes/database")
|
||||
async def funpack_scene_database_save(request):
|
||||
key = normalize_refinement_key(request.query.get("key", ""))
|
||||
body = await request.json()
|
||||
if not isinstance(body, dict):
|
||||
return web.json_response({"error": "Database payload must be an object."}, status=400)
|
||||
|
||||
incoming = body.get("universal_memory", body.get("memory", {}))
|
||||
memory = normalize_scene_memory_items(incoming)
|
||||
data = load_scene_db(key)
|
||||
data["universal_memory"] = memory
|
||||
save_scene_db(data, key)
|
||||
return web.json_response({"saved": len(memory), "data": data, "memory": scene_memory_items(data)})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/funpack/scenes/import")
|
||||
async def funpack_scenes_import(request):
|
||||
key = normalize_refinement_key(request.query.get("key", ""))
|
||||
@@ -784,9 +951,9 @@ async def funpack_scenes_import(request):
|
||||
imported_memory = incoming.get("universal_memory", {})
|
||||
if isinstance(imported_memory, dict):
|
||||
memory = data.setdefault("universal_memory", {})
|
||||
for key, item in imported_memory.items():
|
||||
for memory_key, item in imported_memory.items():
|
||||
if isinstance(item, dict):
|
||||
memory[str(key)] = item
|
||||
memory[str(memory_key)] = item
|
||||
|
||||
imported_scenes = incoming.get("scenes", {})
|
||||
if not isinstance(imported_scenes, dict):
|
||||
@@ -882,14 +1049,11 @@ class FunPackRefinementKeyLoader:
|
||||
|
||||
class FunPackSceneBuilder:
|
||||
CATEGORY = "FunPack/Scene"
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING", "SIGMAS", "FUNPACK_LORA_STACK", "STRING", "STRING")
|
||||
RETURN_TYPES = ("STRING", "STRING", "FUNPACK_LORA_STACK", "STRING")
|
||||
RETURN_NAMES = (
|
||||
"positive_prompt",
|
||||
"negative_prompt",
|
||||
"scene_name",
|
||||
"sigmas",
|
||||
"lora_stack",
|
||||
"refinement_key",
|
||||
"status",
|
||||
)
|
||||
FUNCTION = "build_scene"
|
||||
@@ -900,11 +1064,21 @@ class FunPackSceneBuilder:
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"scene": (scene_names(), {"default": SCENE_NONE}),
|
||||
"scene_name": ("STRING", {"default": "", "multiline": False}),
|
||||
"mode": (["Manual", "Auto", "Learning"], {"default": "Manual"}),
|
||||
"scene": (scene_names(), {"default": SCENE_NONE}),
|
||||
"aliases": ("STRING", {"default": "", "multiline": False}),
|
||||
"action": (["load", "save", "update", "delete"], {"default": "load"}),
|
||||
"output_mode": (["Manual", "Auto", "Learning"], {"default": "Manual"}),
|
||||
"scene_positive": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Write or arrange positive scene words and phrases here.",
|
||||
}),
|
||||
"scene_negative": ("STRING", {
|
||||
"default": "",
|
||||
"multiline": True,
|
||||
"placeholder": "Write or arrange negative scene words and phrases here.",
|
||||
}),
|
||||
"refinement_key": ("STRING", {"default": "", "multiline": False}),
|
||||
"scene_payload": ("STRING", {"default": "{}", "multiline": False}),
|
||||
},
|
||||
@@ -933,7 +1107,6 @@ class FunPackSceneBuilder:
|
||||
"forceInput": True,
|
||||
"tooltip": "Optional linked refinement key, for example from FunPack Refinement Key Loader.",
|
||||
}),
|
||||
"sigmas": ("SIGMAS",),
|
||||
"lora_stack": ("FUNPACK_LORA_STACK", {
|
||||
"tooltip": "Optional current LoRA stack. Scene Builder passes it through unchanged so Refiner can use it for suggestions.",
|
||||
}),
|
||||
@@ -948,38 +1121,17 @@ class FunPackSceneBuilder:
|
||||
def VALIDATE_INPUTS(cls, scene=None, **kwargs):
|
||||
return True
|
||||
|
||||
def _empty_sigmas(self):
|
||||
return torch.FloatTensor([])
|
||||
|
||||
def _manual_scene(self, selected_name, scene_name, aliases, output_mode, refinement_key, payload, sigmas=None):
|
||||
def _manual_scene(self, selected_name, scene_name, aliases, mode, scene_positive, scene_negative, refinement_key, payload):
|
||||
target_name = normalize_scene_name(scene_name) or selected_name
|
||||
scene = {
|
||||
"name": target_name,
|
||||
"aliases": aliases_from_text(aliases),
|
||||
"output_mode": output_mode if output_mode in {"Manual", "Auto", "Learning"} else "Manual",
|
||||
"refinement_key": normalize_refinement_key(refinement_key),
|
||||
"positive_phrases": payload["positive_phrases"],
|
||||
"negative_phrases": payload["negative_phrases"],
|
||||
}
|
||||
if isinstance(sigmas, torch.Tensor) and sigmas.numel() > 0:
|
||||
scene["sigmas"] = tensor_to_serializable(sigmas.detach().cpu())
|
||||
return scene
|
||||
positive_text = scene_text_or_payload(scene_positive, payload["positive_phrases"])
|
||||
negative_text = scene_text_or_payload(scene_negative, payload["negative_phrases"])
|
||||
return scene_from_texts(target_name, aliases, mode, positive_text, negative_text, refinement_key)
|
||||
|
||||
def _scene_sigmas(self, scene, fallback=None):
|
||||
if isinstance(scene.get("sigmas"), dict):
|
||||
try:
|
||||
return serializable_to_tensor(scene["sigmas"]).detach().clone().cpu()
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(fallback, torch.Tensor):
|
||||
return fallback.detach().clone().cpu()
|
||||
return self._empty_sigmas()
|
||||
|
||||
def _outputs_for_scene(self, name, scene, sigmas=None, lora_stack=None, source="Manual"):
|
||||
positive = scene_text_from_phrases(scene.get("positive_phrases", []))
|
||||
negative = scene_text_from_phrases(scene.get("negative_phrases", []))
|
||||
refinement_key = normalize_refinement_key(scene.get("refinement_key", ""))
|
||||
output_sigmas = self._scene_sigmas(scene, sigmas)
|
||||
def _outputs_for_scene(self, name, scene, scene_db=None, lora_stack=None, source="Manual"):
|
||||
positive = str(scene.get("positive_text") or scene_text_from_phrases(scene.get("positive_phrases", [])))
|
||||
negative = str(scene.get("negative_text") or scene_text_from_phrases(scene.get("negative_phrases", [])))
|
||||
positive = resolve_scene_database_wildcards(positive, scene_db)
|
||||
negative = resolve_scene_database_wildcards(negative, scene_db)
|
||||
lora_count = len(lora_stack.get("loras", [])) if isinstance(lora_stack, dict) else 0
|
||||
status = (
|
||||
f"Scene Builder {source}: '{name or scene.get('name', '') or 'unsaved'}'. "
|
||||
@@ -988,12 +1140,11 @@ class FunPackSceneBuilder:
|
||||
f"Negative phrases: {len(scene.get('negative_phrases', []) or [])}. "
|
||||
f"LoRA stack pass-through: {lora_count} LoRA(s)."
|
||||
)
|
||||
return (positive, negative, name or scene.get("name", ""), output_sigmas, lora_stack, refinement_key, status)
|
||||
return (positive, negative, lora_stack, status)
|
||||
|
||||
def _learning_outputs(self, positive_prompt, negative_prompt, refinement_key, sigmas=None, lora_stack=None):
|
||||
def _learning_outputs(self, positive_prompt, negative_prompt, lora_stack=None):
|
||||
positive = str(positive_prompt or "")
|
||||
negative = str(negative_prompt or "")
|
||||
output_sigmas = sigmas.detach().clone().cpu() if isinstance(sigmas, torch.Tensor) else self._empty_sigmas()
|
||||
lora_count = len(lora_stack.get("loras", [])) if isinstance(lora_stack, dict) else 0
|
||||
status = (
|
||||
"Scene Builder Learning: pass-through active. "
|
||||
@@ -1001,34 +1152,36 @@ class FunPackSceneBuilder:
|
||||
f"Collected negative source: {'yes' if negative else 'no'}. "
|
||||
f"LoRA stack pass-through: {lora_count} LoRA(s)."
|
||||
)
|
||||
return (positive, negative, "", output_sigmas, lora_stack, normalize_refinement_key(refinement_key), status)
|
||||
return (positive, negative, lora_stack, status)
|
||||
|
||||
def _empty_outputs(self, status):
|
||||
return ("", "", "", self._empty_sigmas(), None, "", status)
|
||||
return ("", "", None, status)
|
||||
|
||||
def build_scene(
|
||||
self,
|
||||
scene,
|
||||
scene_name,
|
||||
aliases,
|
||||
action,
|
||||
output_mode,
|
||||
refinement_key,
|
||||
scene_payload,
|
||||
mode="Manual",
|
||||
scene=SCENE_NONE,
|
||||
aliases="",
|
||||
action="load",
|
||||
scene_positive="",
|
||||
scene_negative="",
|
||||
refinement_key="",
|
||||
scene_payload="{}",
|
||||
intent_prompt="",
|
||||
positive_prompt="",
|
||||
negative_prompt="",
|
||||
refinement_key_input="",
|
||||
sigmas=None,
|
||||
lora_stack=None,
|
||||
mode="ltx2",
|
||||
output_mode=None,
|
||||
):
|
||||
del mode
|
||||
linked_key = normalize_refinement_key(refinement_key_input)
|
||||
if linked_key:
|
||||
refinement_key = linked_key
|
||||
selected_name = normalize_scene_name(scene)
|
||||
output_mode = output_mode if output_mode in {"Manual", "Auto", "Learning"} else "Manual"
|
||||
if output_mode in {"Manual", "Auto", "Learning"}:
|
||||
mode = output_mode
|
||||
mode = mode if mode in {"Manual", "Auto", "Learning"} else "Manual"
|
||||
action = action if action in {"load", "save", "update", "delete"} else "load"
|
||||
payload = parse_scene_payload(scene_payload)
|
||||
|
||||
@@ -1039,10 +1192,11 @@ class FunPackSceneBuilder:
|
||||
selected_name,
|
||||
scene_name,
|
||||
aliases,
|
||||
output_mode,
|
||||
mode,
|
||||
scene_positive,
|
||||
scene_negative,
|
||||
refinement_key,
|
||||
payload,
|
||||
sigmas,
|
||||
)
|
||||
|
||||
if action == "delete":
|
||||
@@ -1067,7 +1221,7 @@ class FunPackSceneBuilder:
|
||||
manual["updated_at"] = now_iso()
|
||||
scenes[target_name] = manual
|
||||
save_scene_db(data, refinement_key)
|
||||
return self._outputs_for_scene(target_name, manual, sigmas, lora_stack, "Manual saved")
|
||||
return self._outputs_for_scene(target_name, manual, data, lora_stack, "Manual saved")
|
||||
|
||||
if action == "update":
|
||||
target_name = selected_name or normalize_scene_name(scene_name)
|
||||
@@ -1081,20 +1235,20 @@ class FunPackSceneBuilder:
|
||||
manual["updated_at"] = now_iso()
|
||||
scenes[target_name] = manual
|
||||
save_scene_db(data, refinement_key)
|
||||
return self._outputs_for_scene(target_name, manual, sigmas, lora_stack, "Manual updated")
|
||||
return self._outputs_for_scene(target_name, manual, data, lora_stack, "Manual updated")
|
||||
|
||||
if output_mode == "Learning":
|
||||
if mode == "Learning":
|
||||
if memory_changed:
|
||||
save_scene_db(data, refinement_key)
|
||||
return self._learning_outputs(positive_prompt, negative_prompt, refinement_key, sigmas, lora_stack)
|
||||
return self._learning_outputs(positive_prompt, negative_prompt, lora_stack)
|
||||
|
||||
if output_mode == "Auto":
|
||||
if mode == "Auto":
|
||||
matched_name, matched_scene, match_type = find_scene_for_intent(intent_prompt, scenes)
|
||||
if matched_name and isinstance(matched_scene, dict):
|
||||
if memory_changed:
|
||||
save_scene_db(data, refinement_key)
|
||||
return self._outputs_for_scene(matched_name, matched_scene, sigmas, lora_stack, f"Auto {match_type}")
|
||||
return self._outputs_for_scene(matched_name, matched_scene, data, lora_stack, f"Auto {match_type}")
|
||||
|
||||
if memory_changed:
|
||||
save_scene_db(data, refinement_key)
|
||||
return self._outputs_for_scene(manual.get("name", ""), manual, sigmas, lora_stack, "Manual")
|
||||
return self._outputs_for_scene(manual.get("name", ""), manual, data, lora_stack, "Manual")
|
||||
|
||||
+109
-19
@@ -25,7 +25,7 @@ sys.modules.setdefault(
|
||||
|
||||
import templates
|
||||
from conditioning import FunPackVideoRefinerV2
|
||||
from templates import FunPackSceneBuilder, load_scene_db
|
||||
from templates import FunPackSceneBuilder, load_scene_db, normalize_scene_memory_items, save_scene_db
|
||||
|
||||
|
||||
def use_tmp_scene_store(monkeypatch, tmp_path):
|
||||
@@ -56,12 +56,12 @@ def test_scene_builder_collects_inputs_as_memory_but_outputs_selection(monkeypat
|
||||
"loras": [{"name": "vampire.safetensors", "model_weight": 0.8}],
|
||||
}
|
||||
|
||||
positive, negative, scene_name, _, lora_stack, refinement_key, status = builder.build_scene(
|
||||
positive, negative, lora_stack, status = builder.build_scene(
|
||||
scene="-None-",
|
||||
scene_name="Vampire Courtyard",
|
||||
aliases="gothic vampire",
|
||||
action="save",
|
||||
output_mode="Manual",
|
||||
mode="Manual",
|
||||
intent_prompt="",
|
||||
positive_prompt="woman in courtyard, red dress, moonlight",
|
||||
negative_prompt="blurry, bad hands",
|
||||
@@ -72,8 +72,6 @@ def test_scene_builder_collects_inputs_as_memory_but_outputs_selection(monkeypat
|
||||
|
||||
assert positive == "red dress, walking pose"
|
||||
assert negative == "bad hands"
|
||||
assert scene_name == "Vampire Courtyard"
|
||||
assert refinement_key == "scene_key"
|
||||
assert "moonlight" not in positive
|
||||
assert lora_stack is input_lora_stack
|
||||
assert "Manual saved" in status
|
||||
@@ -84,6 +82,62 @@ def test_scene_builder_collects_inputs_as_memory_but_outputs_selection(monkeypat
|
||||
assert data["scenes"]["Vampire Courtyard"]["positive_phrases"] == ["red dress", "walking pose"]
|
||||
|
||||
|
||||
def test_scene_builder_public_interface_is_prompt_stack_status_only():
|
||||
assert FunPackSceneBuilder.RETURN_NAMES == (
|
||||
"positive_prompt",
|
||||
"negative_prompt",
|
||||
"lora_stack",
|
||||
"status",
|
||||
)
|
||||
assert FunPackSceneBuilder.RETURN_TYPES == (
|
||||
"STRING",
|
||||
"STRING",
|
||||
"FUNPACK_LORA_STACK",
|
||||
"STRING",
|
||||
)
|
||||
|
||||
inputs = FunPackSceneBuilder.INPUT_TYPES()
|
||||
required = inputs["required"]
|
||||
optional = inputs["optional"]
|
||||
assert "sigmas" not in required
|
||||
assert "sigmas" not in optional
|
||||
assert "positive_conditioning" not in required
|
||||
assert "positive_conditioning" not in optional
|
||||
assert "refinement_key" not in FunPackSceneBuilder.RETURN_NAMES
|
||||
assert "scene_name" not in FunPackSceneBuilder.RETURN_NAMES
|
||||
|
||||
|
||||
def test_scene_builder_manual_outputs_exact_composed_text(monkeypatch, tmp_path):
|
||||
use_tmp_scene_store(monkeypatch, tmp_path)
|
||||
builder = FunPackSceneBuilder()
|
||||
|
||||
outputs = builder.build_scene(
|
||||
scene="-None-",
|
||||
scene_name="Editable Scene",
|
||||
aliases="",
|
||||
action="save",
|
||||
mode="Manual",
|
||||
scene_positive="red dress. walking pose, custom order",
|
||||
scene_negative="bad hands. blur",
|
||||
positive_prompt="source memory only, should not output",
|
||||
negative_prompt="source negative only",
|
||||
refinement_key="scene_key",
|
||||
scene_payload=json.dumps({
|
||||
"positive_phrases": ["ignored payload phrase"],
|
||||
"negative_phrases": ["ignored payload negative"],
|
||||
}),
|
||||
)
|
||||
|
||||
assert outputs[0] == "red dress. walking pose, custom order"
|
||||
assert outputs[1] == "bad hands. blur"
|
||||
assert "source memory only" not in outputs[0]
|
||||
data = load_scene_db("scene_key")
|
||||
scene = data["scenes"]["Editable Scene"]
|
||||
assert scene["positive_text"] == "red dress. walking pose, custom order"
|
||||
assert scene["negative_text"] == "bad hands. blur"
|
||||
assert "source memory only" in data["universal_memory"]
|
||||
|
||||
|
||||
def test_scene_builder_auto_exact_and_fuzzy_match_saved_scene(monkeypatch, tmp_path):
|
||||
use_tmp_scene_store(monkeypatch, tmp_path)
|
||||
builder = FunPackSceneBuilder()
|
||||
@@ -101,7 +155,7 @@ def test_scene_builder_auto_exact_and_fuzzy_match_saved_scene(monkeypatch, tmp_p
|
||||
scene_name="Forest Dragon",
|
||||
aliases="woodland beast",
|
||||
action="save",
|
||||
output_mode="Manual",
|
||||
mode="Manual",
|
||||
intent_prompt="",
|
||||
positive_prompt="anthro dragon, mossy scales",
|
||||
negative_prompt="extra limbs",
|
||||
@@ -114,7 +168,7 @@ def test_scene_builder_auto_exact_and_fuzzy_match_saved_scene(monkeypatch, tmp_p
|
||||
scene_name="",
|
||||
aliases="",
|
||||
action="load",
|
||||
output_mode="Auto",
|
||||
mode="Auto",
|
||||
intent_prompt="please apply woodland beast now",
|
||||
positive_prompt="ignored prompt",
|
||||
negative_prompt="ignored negative",
|
||||
@@ -123,15 +177,15 @@ def test_scene_builder_auto_exact_and_fuzzy_match_saved_scene(monkeypatch, tmp_p
|
||||
)
|
||||
assert exact[0] == "anthro dragon, mossy scales"
|
||||
assert exact[1] == "extra limbs"
|
||||
assert exact[2] == "Forest Dragon"
|
||||
assert "Auto exact" in exact[6]
|
||||
assert exact[2] is None
|
||||
assert "Auto exact" in exact[3]
|
||||
|
||||
fuzzy = builder.build_scene(
|
||||
scene="-None-",
|
||||
scene_name="",
|
||||
aliases="",
|
||||
action="load",
|
||||
output_mode="Auto",
|
||||
mode="Auto",
|
||||
intent_prompt="make it forest and dragon",
|
||||
positive_prompt="ignored prompt",
|
||||
negative_prompt="",
|
||||
@@ -139,7 +193,7 @@ def test_scene_builder_auto_exact_and_fuzzy_match_saved_scene(monkeypatch, tmp_p
|
||||
scene_payload=json.dumps(manual_payload),
|
||||
)
|
||||
assert fuzzy[0] == "anthro dragon, mossy scales"
|
||||
assert "Auto fuzzy" in fuzzy[6]
|
||||
assert "Auto fuzzy" in fuzzy[3]
|
||||
|
||||
|
||||
def test_scene_builder_auto_no_match_falls_back_to_manual(monkeypatch, tmp_path):
|
||||
@@ -155,7 +209,7 @@ def test_scene_builder_auto_no_match_falls_back_to_manual(monkeypatch, tmp_path)
|
||||
scene_name="",
|
||||
aliases="",
|
||||
action="load",
|
||||
output_mode="Auto",
|
||||
mode="Auto",
|
||||
intent_prompt="no known scene here",
|
||||
positive_prompt="source memory only",
|
||||
negative_prompt="bad source memory",
|
||||
@@ -165,7 +219,7 @@ def test_scene_builder_auto_no_match_falls_back_to_manual(monkeypatch, tmp_path)
|
||||
|
||||
assert outputs[0] == "manual only"
|
||||
assert outputs[1] == "manual negative"
|
||||
assert "Manual" in outputs[6]
|
||||
assert "Manual" in outputs[3]
|
||||
|
||||
|
||||
def test_scene_builder_learning_mode_collects_memory_and_passes_inputs_through(monkeypatch, tmp_path):
|
||||
@@ -180,7 +234,7 @@ def test_scene_builder_learning_mode_collects_memory_and_passes_inputs_through(m
|
||||
scene_name="",
|
||||
aliases="",
|
||||
action="load",
|
||||
output_mode="Learning",
|
||||
mode="Learning",
|
||||
intent_prompt="ignored scene trigger",
|
||||
positive_prompt=positive_prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
@@ -194,10 +248,8 @@ def test_scene_builder_learning_mode_collects_memory_and_passes_inputs_through(m
|
||||
|
||||
assert outputs[0] == positive_prompt
|
||||
assert outputs[1] == negative_prompt
|
||||
assert outputs[2] == ""
|
||||
assert outputs[4] is lora_stack
|
||||
assert outputs[5] == "learn_key"
|
||||
assert "Learning" in outputs[6]
|
||||
assert outputs[2] is lora_stack
|
||||
assert "Learning" in outputs[3]
|
||||
|
||||
data = load_scene_db("learn_key")
|
||||
assert "first prompt" in data["universal_memory"]
|
||||
@@ -215,7 +267,7 @@ def test_refiner_reset_preserves_scene_builder_memory_in_refinement_key(monkeypa
|
||||
scene_name="Memory Scene",
|
||||
aliases="",
|
||||
action="save",
|
||||
output_mode="Manual",
|
||||
mode="Manual",
|
||||
positive_prompt="silver hair, rain street",
|
||||
negative_prompt="blur",
|
||||
refinement_key="shared_key",
|
||||
@@ -233,3 +285,41 @@ def test_refiner_reset_preserves_scene_builder_memory_in_refinement_key(monkeypa
|
||||
assert reset_state["global"]["phrase_memory"] == {}
|
||||
assert reset_state["scene_builder"]["scenes"]["Memory Scene"]["positive_phrases"] == ["silver hair"]
|
||||
assert "silver hair" in reset_state["scene_builder"]["universal_memory"]
|
||||
|
||||
|
||||
def test_scene_builder_database_items_keep_categories_and_wildcard_groups():
|
||||
memory = normalize_scene_memory_items([
|
||||
{"text": "red dress", "category": "appearance", "wildcard_group": "outfit"},
|
||||
{"text": "blue dress", "category": "appearance", "wildcard_group": "outfit"},
|
||||
{"text": "bad hands", "category": "negative"},
|
||||
])
|
||||
|
||||
assert memory["red dress"]["category"] == "appearance"
|
||||
assert memory["red dress"]["wildcard_group"] == "outfit"
|
||||
assert memory["bad hands"]["source"] == "negative"
|
||||
|
||||
|
||||
def test_scene_builder_wildcard_group_outputs_one_matching_phrase(monkeypatch, tmp_path):
|
||||
use_tmp_scene_store(monkeypatch, tmp_path)
|
||||
builder = FunPackSceneBuilder()
|
||||
data = load_scene_db("wild_key")
|
||||
data["universal_memory"] = normalize_scene_memory_items([
|
||||
{"text": "red dress", "category": "appearance", "wildcard_group": "outfit"},
|
||||
{"text": "blue dress", "category": "appearance", "wildcard_group": "outfit"},
|
||||
])
|
||||
save_scene_db(data, "wild_key")
|
||||
monkeypatch.setattr(templates.random, "choice", lambda choices: "blue dress")
|
||||
|
||||
outputs = builder.build_scene(
|
||||
scene="-None-",
|
||||
scene_name="Wildcard Scene",
|
||||
aliases="",
|
||||
action="load",
|
||||
mode="Manual",
|
||||
scene_positive="red dress, blue dress, walking pose",
|
||||
scene_negative="",
|
||||
refinement_key="wild_key",
|
||||
)
|
||||
|
||||
assert outputs[0] == "blue dress, walking pose"
|
||||
assert "red dress" not in outputs[0]
|
||||
|
||||
+761
-397
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user