From b4098aee2a42d6501ca184b3998de6f74e3ecdce Mon Sep 17 00:00:00 2001 From: digital-garbage <133010426+digital-garbage@users.noreply.github.com> Date: Thu, 14 May 2026 12:28:17 +0300 Subject: [PATCH] Simplify scene builder wildcards --- CHANGELOG.md | 2 +- README.md | 2 +- docs/FunPackSceneBuilder.md | 6 ++--- templates.py | 52 +++++++++++++++++++----------------- tests/test_scene_builder.py | 29 +++++++++++++------- web/funpack_scene_builder.js | 51 ++++++++++++++++++++++++++--------- 6 files changed, 90 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7839a1cf..99a3290e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.md b/README.md index 59aa6fc2..380621e3 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/FunPackSceneBuilder.md b/docs/FunPackSceneBuilder.md index 1c0da7a3..27b55e0a 100644 --- a/docs/FunPackSceneBuilder.md +++ b/docs/FunPackSceneBuilder.md @@ -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. diff --git a/templates.py b/templates.py index 8fee4572..885e1fba 100644 --- a/templates.py +++ b/templates.py @@ -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 diff --git a/tests/test_scene_builder.py b/tests/test_scene_builder.py index 7dd2d373..949353db 100644 --- a/tests/test_scene_builder.py +++ b/tests/test_scene_builder.py @@ -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 diff --git a/web/funpack_scene_builder.js b/web/funpack_scene_builder.js index 02a76358..cb0f73df 100644 --- a/web/funpack_scene_builder.js +++ b/web/funpack_scene_builder.js @@ -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;