Simplify scene builder wildcards

This commit is contained in:
digital-garbage
2026-05-14 12:28:17 +03:00
parent 2d1e4313d3
commit b4098aee2a
6 changed files with 90 additions and 52 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ Simplified `FunPack Scene Builder` so prompt and intent text are connection-only
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 button-driven node with centered editor menus for scene name, mode, aliases, Positive Prompt, Negative Prompt, and Database controls. Connected prompts now teach useful words as well as phrase chunks, and the editor refreshes the selected refinement-key database before opening.
Redesigned `FunPack Scene Builder` as a compact button-driven node with centered editor menus for scene name, mode, aliases, Positive Prompt, Negative Prompt, and Database controls. Connected prompts now teach useful words as well as phrase chunks, the editor refreshes the selected refinement-key database before opening, and wildcard random choice is now a checkbox for adjacent entries instead of a text group.
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.
+1 -1
View File
@@ -8,7 +8,7 @@ Added `FunPack Scene Builder`, a replacement for `FunPack Template Manager`. It
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 button-driven node with centered editor menus for scene name, mode, aliases, Positive Prompt, Negative Prompt, and Database controls. Prompt editors preserve freely typed text while phrase chips can be clicked or dragged in from the database, and connected prompts now teach useful words as well as phrase chunks.
Redesigned Scene Builder as a compact button-driven node with centered editor menus for scene name, mode, aliases, Positive Prompt, Negative Prompt, and Database controls. Prompt editors preserve freely typed text while phrase chips can be clicked or dragged in from the database, connected prompts now teach useful words as well as phrase chunks, and wildcard random choice is a simple checkbox.
Added searchable LoRA selection to `FunPack Apply LoRA Weights` while keeping the compact row UI and serialized LoRA stack format.
+3 -3
View File
@@ -12,7 +12,7 @@ The node surface is a compact launcher. Scene name, mode, aliases, prompt editor
**Negative prompt**: Same composer behavior for the negative prompt.
**Database**: Add, edit, delete, search, categorize, and wildcard-group universal words and phrases.
**Database**: Add, edit, delete, search, categorize, and mark universal words and phrases as wildcards.
The editor refreshes the selected scene database before opening. When `refinement_key_input` is connected to a Refinement Key Loader, the editor reads that linked key so prompt memory appears in the same database where queue runs stored it.
@@ -50,12 +50,12 @@ Scene Builder memory is stored in the selected refinement key under its own `sce
## 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.
Wildcard is a checkbox on each database word or phrase. If two or more wildcard-enabled entries appear next to each other in the runtime composed prompt, Scene Builder outputs one randomly selected entry from that adjacent run 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 and word memory.
2. Open **Database** to clean up phrases, assign categories, or set wildcard groups.
2. Open **Database** to clean up phrases, assign categories, or enable wildcard random choice.
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.
+27 -25
View File
@@ -403,7 +403,7 @@ 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(),
"wildcard": bool(item.get("wildcard")) or bool(str(item.get("wildcard_group") or "").strip()),
})
return sorted(items, key=lambda item: (item["category"], item["text"].lower()))
@@ -431,6 +431,7 @@ def normalize_scene_memory_items(value):
if category not in SCENE_CATEGORIES:
category = "negative" if source == "negative" else "details"
tokens = scene_token_words(text)
wildcard = bool(item.get("wildcard")) or bool(str(item.get("wildcard_group") or "").strip())
memory[key] = {
"text": text,
"source": source,
@@ -439,7 +440,7 @@ def normalize_scene_memory_items(value):
"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(),
"wildcard": wildcard,
}
return memory
@@ -679,40 +680,41 @@ def resolve_scene_database_wildcards(text, scene_db):
if not text or not isinstance(scene_db, dict):
return text
groups = {}
wildcard_phrases = {}
for item in scene_db.get("universal_memory", {}).values():
if not isinstance(item, dict):
continue
group = str(item.get("wildcard_group") or "").strip()
wildcard = bool(item.get("wildcard")) or bool(str(item.get("wildcard_group") or "").strip())
phrase = str(item.get("text") or "").strip()
if not group or not phrase:
if not wildcard or not phrase:
continue
groups.setdefault(group, {})[normalize_scene_key(phrase)] = phrase
wildcard_phrases[normalize_scene_key(phrase)] = phrase
if not groups:
if not wildcard_phrases:
return text
parts = re.split(r"([,;.\n]+)", text)
matches_by_group = {}
changed = False
run = []
def flush_run():
nonlocal changed, run
unique_keys = sorted({phrase_key for _, phrase_key in run})
if len(unique_keys) >= 2:
keep = random.choice(unique_keys)
for index, phrase_key in run:
if phrase_key != keep:
parts[index] = ""
changed = True
run = []
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 phrase_key and phrase_key in wildcard_phrases:
run.append((index, phrase_key))
else:
flush_run()
flush_run()
if not changed:
return text
+20 -9
View File
@@ -309,25 +309,28 @@ def test_refiner_reset_preserves_scene_builder_memory_in_refinement_key(monkeypa
assert "silver hair" in reset_state["scene_builder"]["universal_memory"]
def test_scene_builder_database_items_keep_categories_and_wildcard_groups():
def test_scene_builder_database_items_keep_categories_and_wildcards():
memory = normalize_scene_memory_items([
{"text": "red dress", "category": "appearance", "wildcard_group": "outfit"},
{"text": "blue dress", "category": "appearance", "wildcard_group": "outfit"},
{"text": "red dress", "category": "appearance", "wildcard": True},
{"text": "blue dress", "category": "appearance", "wildcard": True},
{"text": "bad hands", "category": "negative"},
])
assert memory["red dress"]["category"] == "appearance"
assert memory["red dress"]["wildcard_group"] == "outfit"
assert memory["red dress"]["wildcard"] is True
assert memory["blue dress"]["wildcard"] is True
assert memory["bad hands"]["wildcard"] is False
assert memory["bad hands"]["source"] == "negative"
def test_scene_builder_wildcard_group_outputs_one_matching_phrase(monkeypatch, tmp_path):
def test_scene_builder_wildcard_outputs_one_adjacent_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"},
{"text": "red dress", "category": "appearance", "wildcard": True},
{"text": "blue dress", "category": "appearance", "wildcard": True},
{"text": "green dress", "category": "appearance", "wildcard": True},
])
save_scene_db(data, "wild_key")
monkeypatch.setattr(templates.random, "choice", lambda choices: "blue dress")
@@ -338,10 +341,18 @@ def test_scene_builder_wildcard_group_outputs_one_matching_phrase(monkeypatch, t
aliases="",
action="load",
mode="Manual",
scene_positive="red dress, blue dress, walking pose",
scene_positive="red dress, blue dress, walking pose, green dress",
scene_negative="",
refinement_key="wild_key",
)
assert outputs[0] == "blue dress, walking pose"
assert outputs[0] == "blue dress, walking pose, green dress"
assert "red dress" not in outputs[0]
def test_scene_builder_legacy_wildcard_group_migrates_to_checkbox():
memory = normalize_scene_memory_items([
{"text": "red dress", "category": "appearance", "wildcard_group": "outfit"},
])
assert memory["red dress"]["wildcard"] is True
+38 -13
View File
@@ -150,7 +150,7 @@ function normalizeMemoryMap(items) {
count: Number.isFinite(Number(item.count)) ? Number(item.count) : 0,
created_at: item.created_at || new Date().toISOString(),
updated_at: new Date().toISOString(),
wildcard_group: String(item.wildcard_group || "").trim(),
wildcard: Boolean(item.wildcard || String(item.wildcard_group || "").trim()),
};
}
return output;
@@ -394,6 +394,18 @@ function panelSelect(values, selected) {
return element;
}
function panelCheckbox(checked, label) {
const wrapper = document.createElement("label");
wrapper.className = "funpack-scene-checkbox";
const input = document.createElement("input");
input.type = "checkbox";
input.checked = Boolean(checked);
const span = document.createElement("span");
span.textContent = label;
wrapper.append(input, span);
return { wrapper, input };
}
function phraseButton(text, onInsert) {
const element = document.createElement("button");
element.type = "button";
@@ -715,8 +727,7 @@ function renderDatabaseEditor(panel, node) {
option.textContent = category;
addCategory.append(option);
}
const addWildcard = document.createElement("input");
addWildcard.placeholder = "Wildcard group";
const addWildcard = panelCheckbox(false, "Wildcard");
const add = panelButton("Add", "primary");
add.addEventListener("click", () => {
const text = addText.value.trim();
@@ -729,12 +740,13 @@ function renderDatabaseEditor(panel, node) {
source: addCategory.value === "negative" ? "negative" : "positive",
category: addCategory.value,
count: 0,
wildcard_group: addWildcard.value.trim(),
wildcard: addWildcard.input.checked,
});
addText.value = "";
addWildcard.input.checked = false;
renderPanel(panel, node, "database");
});
tools.append(addText, addCategory, addWildcard, add);
tools.append(addText, addCategory, addWildcard.wrapper, add);
body.append(tools);
const search = document.createElement("input");
@@ -772,18 +784,16 @@ function renderDatabaseEditor(panel, node) {
item.category = category.value;
item.source = category.value === "negative" ? "negative" : "positive";
});
const wildcard = document.createElement("input");
wildcard.value = item.wildcard_group || "";
wildcard.placeholder = "Wildcard";
wildcard.addEventListener("input", () => {
item.wildcard_group = wildcard.value.trim();
const wildcard = panelCheckbox(Boolean(item.wildcard || item.wildcard_group), "Wildcard");
wildcard.input.addEventListener("change", () => {
item.wildcard = wildcard.input.checked;
});
const del = panelButton("Delete", "danger");
del.addEventListener("click", () => {
panel.databaseItems = panel.databaseItems.filter((candidate) => candidate !== item);
renderList();
});
row.append(text, category, wildcard, del);
row.append(text, category, wildcard.wrapper, del);
list.append(row);
});
};
@@ -986,6 +996,21 @@ function injectStyles() {
color: #f2f2f2;
outline: none;
}
.funpack-scene-db-tools .funpack-scene-checkbox input,
.funpack-scene-db-row .funpack-scene-checkbox input {
width: auto;
min-height: 0;
padding: 0;
}
.funpack-scene-checkbox {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
min-height: 28px;
color: #d9dee5;
white-space: nowrap;
}
.funpack-scene-search { margin: 8px 0; }
.funpack-scene-bank,
.funpack-scene-db-list {
@@ -1012,13 +1037,13 @@ function injectStyles() {
}
.funpack-scene-db-tools {
display: grid;
grid-template-columns: minmax(0, 1.6fr) 110px minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1.6fr) 110px auto auto;
gap: 6px;
align-items: center;
}
.funpack-scene-db-row {
display: grid;
grid-template-columns: minmax(0, 1.7fr) 110px minmax(0, 0.9fr) auto;
grid-template-columns: minmax(0, 1.7fr) 110px auto auto;
gap: 6px;
align-items: center;
padding: 5px 0;