Refine Scene Builder editor

This commit is contained in:
digital-garbage
2026-05-13 23:00:40 +03:00
parent 71aad9e3d4
commit 789f56a461
6 changed files with 1131 additions and 513 deletions
+3 -1
View File
@@ -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.
+3 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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]
File diff suppressed because it is too large Load Diff