From 65816e353a640d4cf78717bbfa6a245add0b3e4c Mon Sep 17 00:00:00 2001
From: digital-garbage <133010426+digital-garbage@users.noreply.github.com>
Date: Mon, 15 Jun 2026 17:15:28 +0300
Subject: [PATCH 1/7] Stage 2 (1/3): remove the prompt-repair application +
param
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Neutralize the repair application in refine(): the intent-aligned prompt is
encoded as-is — perfect-repair phrase injection, missing-axis repair, and the
emphasis pass are gone. The advisor remains the only prompt-rewriting path, and
repair_feedback/active_repair_axes is KEPT (it's the 'what's missing' signal the
advisor still reads). Drop the prompt_repair param from refine(), INPUT_TYPES,
FunPackStudio, and both UIs (web + engine_settings).
The dead repair-only methods (_v2_repair_prompt_for_missing_axes, perfect-repair
phrases, etc.) come out in the next commit; shared helpers like
_v2_repair_intent_roots (used by intent alignment) stay.
Deleted the now-obsolete test_repair_candidates_saved_as_json_lists (tested the
removed injection). No new test failures vs dev.
Co-Authored-By: Claude Opus 4.8
---
conditioning.py | 51 +++++-------------------
movie_editor/frontend/engine_settings.js | 1 -
tests/test_v2_category_alignment.py | 23 -----------
web/funpack_studio.js | 6 +--
4 files changed, 11 insertions(+), 70 deletions(-)
diff --git a/conditioning.py b/conditioning.py
index 3a1e5cc8..08cd60f4 100644
--- a/conditioning.py
+++ b/conditioning.py
@@ -6379,11 +6379,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
"label": "I'm Feeling Lucky",
"tooltip": "Compose a learned prompt from V2 phrase memory, then encode it through the connected CLIP.",
}),
- "prompt_repair": ("BOOLEAN", {
- "default": True,
- "label": "Prompt Repair",
- "tooltip": "Allow V2 to append learned phrases for missing axes. Disable when not enough context has been built yet or when memory suggestions are disrupting the generation.",
- }),
"advisor_thinking": ("BOOLEAN", {
"default": True,
"label": "Advisor Thinking",
@@ -11981,7 +11976,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
reset_session=False, lora_stack=None, im_feeling_lucky=False, user_intent_prompt="",
refinement_key_input="", positive_conditioning=None, clip_vision_output=None,
source_image=None, model=None, mode="Refine", advisor_mode="Off", advisor_thinking=True,
- advisor_clip=None, feedback_prompt="", prompt_repair=True, temporal_style="natural",
+ advisor_clip=None, feedback_prompt="", temporal_style="natural",
split_by_transitions=False, split_transition_placement="start", reference_injection=False,
value_guidance=True, latent=None, seed_output_connected=False,
steer_mode="relative", absolute_strength=0.6,
@@ -12313,40 +12308,16 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
intent_phrases,
global_state,
)
- if prompt_repair:
- aligned_prompt, perfect_repair_status, perfect_repair_adjustments = self._v2_apply_perfect_repair_phrases(
- aligned_prompt,
- current_family_slot,
- intent_prompt=intent_source_prompt,
- intent_phrases=intent_phrases,
- clip=clip,
- encode_cache=encode_cache,
- )
- prompt_to_encode, emphasis_status = self._v2_emphasized_prompt(
- aligned_prompt,
- phrases,
- global_state,
- {**learning_profile, "missing_axes": repair_feedback.get("missing_axes", [])},
- )
- else:
- perfect_repair_status = "Perfect repairs: disabled."
- perfect_repair_adjustments = []
- prompt_to_encode = aligned_prompt
- emphasis_status = "Prompt emphasis: disabled."
+ # Prompt repair removed (Stage 2). The intent-aligned prompt is encoded as-is; the only
+ # remaining prompt-rewriting path is the advisor (when enabled). Perfect-repair phrase
+ # injection, missing-axis repair, and the emphasis pass are gone. repair_feedback /
+ # active_repair_axes is KEPT — it's the "what's missing" signal the advisor still reads.
+ perfect_repair_status = "Perfect repairs: removed."
+ perfect_repair_adjustments = []
+ prompt_to_encode = aligned_prompt
+ emphasis_status = "Prompt emphasis: removed."
advisor_active = advisor_mode != "Off"
- if prompt_repair:
- prompt_to_encode, repair_status, repair_candidates = self._v2_repair_prompt_for_missing_axes(
- prompt_to_encode,
- phrases,
- global_state,
- previous_run,
- repair_feedback,
- intent_phrases=intent_phrases,
- intent_family_slot=current_family_slot,
- apply=not advisor_active,
- )
- else:
- repair_status = "Prompt repair: disabled."
+ repair_status = "Prompt repair: removed."
prompt_to_encode, wildcard_status = self._v2_resolve_scene_builder_wildcards(
prompt_to_encode,
scene_db,
@@ -14529,7 +14500,6 @@ class FunPackStudio:
mode = str(rf.get("mode", "Refine") or "Refine")
advisor_mode = str(rf.get("advisor_mode", "Off") or "Off")
advisor_thinking = bool(rf.get("advisor_thinking", True))
- prompt_repair = bool(rf.get("prompt_repair", True))
im_feeling_lucky = bool(rf.get("im_feeling_lucky", False))
reset_session = bool(rf.get("reset_session", False))
temporal_style = str(rf.get("temporal_style", "natural") or "natural").strip().lower()
@@ -14688,7 +14658,6 @@ class FunPackStudio:
advisor_thinking=advisor_thinking,
advisor_clip=advisor_clip,
feedback_prompt=feedback_prompt,
- prompt_repair=prompt_repair,
temporal_style=temporal_style,
split_by_transitions=split_by_transitions,
split_transition_placement=split_transition_placement,
diff --git a/movie_editor/frontend/engine_settings.js b/movie_editor/frontend/engine_settings.js
index 1e67afd9..5e2bd903 100644
--- a/movie_editor/frontend/engine_settings.js
+++ b/movie_editor/frontend/engine_settings.js
@@ -66,7 +66,6 @@
const STUDIO_REFINER_ESSENTIALS = [
{ name: "vision_conditioning", label: "Vision conditioning", default: true },
{ name: "reference_injection", label: "Reference injection", default: false },
- { name: "prompt_repair", label: "Prompt repair", default: false },
];
const STUDIO_REFINER_ADVANCED = [
{ name: "value_guidance", label: "Value guidance", kind: "bool", default: true },
diff --git a/tests/test_v2_category_alignment.py b/tests/test_v2_category_alignment.py
index 5e2db24f..31ee5e6b 100644
--- a/tests/test_v2_category_alignment.py
+++ b/tests/test_v2_category_alignment.py
@@ -2177,29 +2177,6 @@ def test_phrase_clusters_train_more_strongly_than_ngrams_and_tokens():
assert phrase_score > ngram_score > token_score
-def test_repair_candidates_saved_as_json_lists(tmp_path):
- refiner = FunPackVideoRefinerV2()
- state_path = tmp_path / "state.json"
- refiner._v2_state_path = lambda refinement_key: str(state_path)
- rich_prompt = "woman, walking, reaching hand, tiny particles, rain reflections"
-
- refiner.refine_v2(rich_prompt, FakeClip(), "Perfect", "json-repair-test")
- refiner.refine_v2(rich_prompt, FakeClip(), "Perfect", "json-repair-test")
- refiner.refine_v2(
- "woman, rain",
- FakeClip(),
- "Missing details + action",
- "json-repair-test",
- user_intent_prompt=rich_prompt,
- )
-
- state = json.loads(state_path.read_text(encoding="utf-8"))
- candidates = state["last_run"]["repair_candidates"]
- assert candidates
- assert isinstance(candidates[0]["axes"], list)
- assert "walking" in state["last_run"]["encoded_prompt"]
-
-
def test_wrong_action_rating_preserves_quality_but_marks_action_wrong():
refiner = FunPackVideoRefinerV2()
global_state = {"phrase_memory": {}, "preferred_context_memory": {}}
diff --git a/web/funpack_studio.js b/web/funpack_studio.js
index e02e56f6..9c24a325 100644
--- a/web/funpack_studio.js
+++ b/web/funpack_studio.js
@@ -69,7 +69,7 @@ function defaultSettings() {
refinement_key: "",
overrides: { refinement_key: false, feedback_prompt: false, user_intent_prompt: false, negative_prompt: false },
scene_builder: { mode: "Pass-through", scene: NONE_SENTINEL, scene_name: "", aliases: "", scene_positive: "", scene_negative: "" },
- refiner: { mode: "Refine", advisor_mode: "Off", advisor_thinking: true, prompt_repair: true, im_feeling_lucky: false, reset_session: false, feedback_prompt: "", user_intent_prompt_override: "", negative_prompt: "", temporal_style: "natural", split_by_transitions: false, split_transition_placement: "start", reference_injection: false, vision_conditioning: true, value_guidance: true, steer_mode: "relative", absolute_strength: 0.6 },
+ refiner: { mode: "Refine", advisor_mode: "Off", advisor_thinking: true, im_feeling_lucky: false, reset_session: false, feedback_prompt: "", user_intent_prompt_override: "", negative_prompt: "", temporal_style: "natural", split_by_transitions: false, split_transition_placement: "start", reference_injection: false, vision_conditioning: true, value_guidance: true, steer_mode: "relative", absolute_strength: 0.6 },
advisor_llm: { enabled: false, model_path: "huihui-ai/Huihui-Qwen3-8B-abliterated-v2", dtype: "bfloat16" },
loras: [],
loras_config: { mode: "ltx2", per_block: false },
@@ -1188,10 +1188,6 @@ function openPanel(node) {
thinkToggle.inp.addEventListener("change", () => { settings.refiner.advisor_thinking = thinkToggle.inp.checked; });
body.append(row("Thinking", thinkToggle.wrap));
- const repairToggle = toggleEl(settings.refiner.prompt_repair, "Prompt repair");
- repairToggle.inp.addEventListener("change", () => { settings.refiner.prompt_repair = repairToggle.inp.checked; });
- body.append(row("Prompt repair", repairToggle.wrap));
-
const luckyToggle = toggleEl(settings.refiner.im_feeling_lucky, "I'm Feeling Lucky");
luckyToggle.inp.addEventListener("change", () => { settings.refiner.im_feeling_lucky = luckyToggle.inp.checked; });
body.append(row("Lucky", luckyToggle.wrap));
From e39a64de5711fcdbfa423dd8cb1339c889cfc43d Mon Sep 17 00:00:00 2001
From: digital-garbage <133010426+digital-garbage@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:25:44 +0300
Subject: [PATCH 2/7] Stage 2 (2/3): delete dead prompt-repair methods + their
tests
Removes the now-unreferenced repair-only machinery left behind after
Stage 2 (1/3) neutralized the application:
- _v2_prompt_repair_text_allowed
- _v2_emphasized_prompt
- _v2_repair_prompt_for_missing_axes and the repair-candidate helpers
- perfect_repairs / rejected_repairs storage + perfect-repair eviction
note in the intent-family learn path
Shared helpers still used by intent-alignment (e.g. _v2_repair_intent_roots,
_v2_repair_candidate_matches_intent, _v2_prompt_contains_text) are kept, as
is the repair_feedback / active_repair_axes "what's missing" signal the
advisor reads.
Deletes the 11 category-alignment tests that exercised the removed methods
directly. No new test failures (the 10/31 remaining are pre-existing env
failures from missing server/comfy modules).
Co-Authored-By: Claude Opus 4.8
---
conditioning.py | 431 +-----------------------
tests/test_v2_category_alignment.py | 490 ----------------------------
2 files changed, 1 insertion(+), 920 deletions(-)
diff --git a/conditioning.py b/conditioning.py
index 08cd60f4..3b130895 100644
--- a/conditioning.py
+++ b/conditioning.py
@@ -7307,13 +7307,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
item["source"] = "scene_builder_user"
return True
- def _v2_prompt_repair_text_allowed(self, text):
- if not self._v2_clean_phrase_text(text):
- return False
- if self._v2_item_has_blocked_auto_category(text):
- return False
- return self._v2_primary_category_for_text(text) in self.AUTO_INJECT_ALLOWED_CATEGORIES
-
def _v2_auto_inject_entry_allowed(self, entry, prompt=""):
text = str(entry.get("text", "") if isinstance(entry, dict) else entry).strip()
if not text:
@@ -8479,25 +8472,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
anchor_count = 0
delta_updates = 0
- if axis_feedback and set(axis_feedback.get("wrong_axes", [])):
- rejected_repairs = slot.setdefault("rejected_repairs", {})
- for repair in repairs:
- text = self._v2_clean_phrase_text(repair.get("text", ""))
- if not text or self._v2_phrase_represented_by(text, intent_phrases):
- continue
- entry = rejected_repairs.setdefault(text, {"text": text, "count": 0, "last_seen_iter": 0})
- entry["count"] = int(entry.get("count", 0)) + 1
- entry["last_seen_iter"] = int(iter_num)
- evicted_repairs = []
- if rating_profile.get("key") in {"awful", "wrong_appearance"} and isinstance(slot.get("perfect_repairs"), dict):
- evict_texts = {
- self._v2_clean_phrase_text(item.get("text", ""))
- for item in (last_run.get("intent_alignment_adjustments") or [])
- if isinstance(item, dict) and item.get("source") == "perfect_repair"
- }
- for text in evict_texts:
- if text and slot["perfect_repairs"].pop(text, None) is not None:
- evicted_repairs.append(text)
if rating_profile.get("key") == "like" and isinstance(last_run.get("conditioning"), dict):
anchor_payload = {
"intent_prompt": source_intent,
@@ -8528,22 +8502,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
if self._v2_store_conditioning_delta_average(positive_delta, delta):
positive_delta["last_seen_iter"] = int(iter_num)
delta_updates += 1
- perfect_repairs = slot.setdefault("perfect_repairs", {})
- for repair in repairs:
- text = self._v2_clean_phrase_text(repair.get("text", ""))
- if not text:
- continue
- entry = perfect_repairs.setdefault(text, {
- "text": text,
- "axes": {},
- "count": 0,
- "last_seen_iter": 0,
- })
- entry["count"] = int(entry.get("count", 0)) + 1
- entry["last_seen_iter"] = int(iter_num)
- axes = entry.setdefault("axes", {})
- for axis in self._v2_order_axes(set(repair.get("axes", []))):
- axes[axis] = int(axes.get(axis, 0)) + 1
if len(variants) > 40:
slot["variant_evidence"] = dict(sorted(
@@ -8557,14 +8515,11 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
)[:40])
self._v2_sync_intent_family_aliases(global_state, family_key, slot)
- eviction_note = (
- f" Perfect-repair evicted: {', '.join(evicted_repairs)}." if evicted_repairs else ""
- )
return (
"Intent family learned: "
f"family={family_key[:8]} sim={similarity:.2f}, repeated preference(s)={preference_updates}, "
f"anchors={anchor_count or len(slot.get('perfect_anchors', {})) + len(slot.get('loved_variants', {}))}, "
- f"variant reward {variant['avg_reward']:+.2f}, delta update(s)={delta_updates}.{eviction_note}"
+ f"variant reward {variant['avg_reward']:+.2f}, delta update(s)={delta_updates}."
), family_key
def _v2_apply_intent_family_delta(self, conditioning, family_slot, strength):
@@ -10144,33 +10099,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
f" Total delta clamped to 7.5% of conditioning norm"
)
- def _v2_emphasized_prompt(self, prompt, phrases, global_state, rating_profile):
- missing_axes = set(global_state.get("last_missing_axes", [])) | set(rating_profile.get("missing_axes", []))
- if not missing_axes:
- return prompt, "Prompt emphasis: none."
- candidates = []
- for phrase in phrases:
- phrase_axes = self._v2_axes_for_scores(
- phrase.get("effective_category_scores", phrase.get("category_scores", {})) or
- {phrase.get("primary", "details"): 1.0}
- )
- if phrase_axes & missing_axes:
- candidates.append(phrase.get("text", ""))
- candidates = [item for item in candidates if item]
- if not candidates:
- return prompt, "Prompt emphasis: no matching current phrases."
- emphasized = []
- seen = set()
- for text in candidates:
- key = text.lower()
- if key in seen:
- continue
- seen.add(key)
- emphasized.append(text)
- if len(emphasized) >= 3:
- break
- return f"{prompt}, {', '.join(emphasized)}", f"Prompt emphasis: repeated {', '.join(emphasized)}."
-
def _v2_prompt_contains_text(self, prompt, text):
prompt_key = re.sub(r"\s+", " ", str(prompt or "").strip().lower())
text_key = re.sub(r"\s+", " ", str(text or "").strip().lower())
@@ -10225,363 +10153,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
scores = self._v2_heuristic_scores(text)
return len(words) <= 3 and not self._v2_axes_for_scores(scores)
- def _v2_repair_reference_context(self, text, reference_phrases):
- candidate_roots = self._v2_repair_intent_roots(text)
- if not candidate_roots:
- return {}
- best_phrase = None
- best_overlap = 0
- for phrase in reference_phrases or []:
- if not isinstance(phrase, dict):
- continue
- overlap = len(candidate_roots & self._v2_repair_intent_roots(phrase.get("text", "")))
- if overlap > best_overlap:
- best_phrase = phrase
- best_overlap = overlap
- if not isinstance(best_phrase, dict):
- return {}
- return best_phrase.get("context", {}) if isinstance(best_phrase.get("context", {}), dict) else {}
-
- def _v2_repair_context_matches_memory(self, text, entry, reference_phrases):
- if not isinstance(entry, dict):
- return True
- context = self._v2_repair_reference_context(text, reference_phrases)
- context_words = context.get("context_words", []) if isinstance(context, dict) else []
- senses = entry.get("context_senses", {})
- if isinstance(senses, dict) and senses:
- sense, similarity = self._v2_best_context_sense(entry, context)
- if isinstance(sense, dict) and similarity >= 0.30:
- return True
- return len(self._v2_phrase_words(text)) > 1 and not context_words
- learned_context = entry.get("context", {})
- learned_words = learned_context.get("context_words", []) if isinstance(learned_context, dict) else []
- if learned_words and context_words:
- return self._v2_context_similarity(learned_context, context) >= 0.30
- if learned_words and len(self._v2_phrase_words(text)) <= 1:
- return False
- return True
-
- def _v2_repair_prompt_for_missing_axes(
- self,
- prompt,
- current_phrases,
- global_state,
- previous_run,
- axis_feedback,
- intent_phrases=None,
- intent_family_slot=None,
- allow_axis_fallback=True,
- apply=True,
- ):
- missing_axes = set(axis_feedback.get("missing_axes", [])) if isinstance(axis_feedback, dict) else set()
- missing_axes = set(self._v2_order_axes(missing_axes))
- if not missing_axes:
- return prompt, "Prompt repair: none.", []
-
- candidates = {}
- intent_roots = self._v2_repair_intent_roots(prompt)
- reference_phrases = [
- phrase
- for phrase in list(current_phrases or []) + list(intent_phrases or [])
- if isinstance(phrase, dict)
- ]
- for item in intent_phrases or []:
- intent_roots |= self._v2_repair_intent_roots(item.get("text", ""))
-
- def candidate_axes(item):
- if not isinstance(item, dict):
- return set()
- axes = self._v2_phrase_axes(item)
- wanted_axes = item.get("wanted_axes", {})
- if isinstance(wanted_axes, dict):
- axes |= {axis for axis, count in wanted_axes.items() if int(count or 0) > 0}
- axis_counts = item.get("axes", {})
- if isinstance(axis_counts, dict):
- axes |= {axis for axis, count in axis_counts.items() if int(count or 0) > 0}
- return axes
-
- def add_candidate(text, axes, score, source, cluster=None, memory_entry=None):
- clean = self._v2_clean_phrase_text(text)
- if not clean:
- return
- if not self._v2_prompt_repair_text_allowed(clean):
- return
- words = self._v2_phrase_words(clean)
- if len(words) > 16:
- return
- axes = set(axes or set()) & missing_axes
- if not axes or self._v2_prompt_contains_text(prompt, clean):
- return
- if source != "intent" and not self._v2_repair_candidate_matches_intent(clean, intent_roots):
- return
- if source != "intent" and not self._v2_repair_context_matches_memory(clean, memory_entry, reference_phrases):
- return
- safe_cluster = [
- self._v2_clean_phrase_text(item)
- for item in (cluster or [])
- if (
- self._v2_prompt_repair_text_allowed(item) and
- self._v2_repair_candidate_matches_intent(item, intent_roots) and
- self._v2_repair_context_matches_memory(item, memory_entry, reference_phrases)
- )
- ]
- safe_cluster = [item for item in safe_cluster if item]
- existing = candidates.get(clean)
- payload = {
- "text": clean,
- "axes": axes,
- "score": float(score),
- "source": source,
- "cluster": safe_cluster,
- }
- if existing is None or payload["score"] > existing["score"]:
- candidates[clean] = payload
-
- for item in intent_phrases or []:
- axes = candidate_axes(item)
- if axes & missing_axes:
- add_candidate(item.get("text", ""), axes, 3.0, "intent")
-
- if isinstance(intent_family_slot, dict):
- for entry in intent_family_slot.get("intent_preference_phrases", {}).values():
- if not isinstance(entry, dict):
- continue
- if int(entry.get("seen_count", 0)) < 2:
- continue
- axes = candidate_axes(entry)
- if not (axes & missing_axes):
- continue
- score = (
- 1.35 +
- min(0.75, int(entry.get("seen_count", 0)) * 0.14) +
- min(0.60, float(entry.get("score", 0.0)) * 0.16)
- )
- add_candidate(
- entry.get("text", ""),
- axes,
- score,
- "intent_preference",
- memory_entry=entry,
- )
-
- if isinstance(previous_run, dict):
- for item in self._v2_concept_units_for_run(previous_run):
- if item.get("kind") == "token":
- continue
- axes = candidate_axes(item)
- if axes & missing_axes:
- add_candidate(item.get("text", ""), axes, 1.75, "previous", memory_entry=item)
-
- preferred_memory = global_state.get("preferred_context_memory", {}) if isinstance(global_state, dict) else {}
- for entry in preferred_memory.values() if isinstance(preferred_memory, dict) else []:
- if not isinstance(entry, dict):
- continue
- axes = candidate_axes(entry)
- if not (axes & missing_axes):
- continue
- cluster = [
- self._v2_clean_phrase_text(item)
- for item in entry.get("phrases", [])
- ]
- cluster = [
- item for item in cluster
- if item and self._v2_prompt_repair_text_allowed(item) and not self._v2_prompt_contains_text(prompt, item)
- ]
- score = (
- 2.0 +
- float(entry.get("score", 0.0)) * 0.34 +
- min(1.8, int(entry.get("liked_count", 0) or 0) * 0.42) +
- min(0.8, int(entry.get("satisfied_count", 0) or 0) * 0.16)
- )
- if cluster:
- for offset, item_text in enumerate(cluster[:8]):
- add_candidate(item_text, axes, score - offset * 0.03, "preferred_context", cluster=cluster[:8], memory_entry=entry)
- else:
- add_candidate(entry.get("anchor", ""), axes, score, "preferred_context", memory_entry=entry)
-
- memory = global_state.get("phrase_memory", {}) if isinstance(global_state, dict) else {}
- for text, entry in memory.items():
- if not isinstance(entry, dict):
- continue
- axes = candidate_axes(entry)
- wanted_axes = entry.get("wanted_axes", {}) if isinstance(entry.get("wanted_axes", {}), dict) else {}
- axis_hits = sum(int(wanted_axes.get(axis, 0) or 0) for axis in missing_axes)
- evidence = int(entry.get("category_evidence_count", 0) or 0)
- score = (
- 1.0 +
- float(entry.get("score", 0.0)) * 0.18 +
- min(1.2, axis_hits * 0.34) +
- min(0.5, evidence * 0.05) +
- min(0.4, int(entry.get("liked_count", 0) or 0) * 0.08) -
- min(1.0, int(entry.get("bad_count", 0) or 0) * 0.20) -
- min(0.9, int(entry.get("wrong_count", 0) or 0) * 0.16)
- )
- if axes & missing_axes and score > 0.60:
- add_candidate(entry.get("text", text), axes, score, "memory", memory_entry=entry)
-
- current_axes = set()
- for phrase in current_phrases or []:
- current_axes |= candidate_axes(phrase)
- missing_not_represented = missing_axes - current_axes
-
- source_priority = {
- "intent": 4,
- "intent_preference": 3,
- "previous": 3,
- "preferred_context": 2,
- "memory": 1,
- }
- ranked = sorted(
- candidates.values(),
- key=lambda item: (
- bool(item["axes"] & missing_not_represented),
- source_priority.get(item["source"], 0),
- item["score"],
- len(item["text"]),
- ),
- reverse=True,
- )
- selected = []
- selected_texts = set()
- seen_axes = set()
- for item in ranked:
- cluster = item.get("cluster", []) if item.get("source") == "preferred_context" else []
- for cluster_text in cluster:
- if (
- cluster_text in selected_texts or
- self._v2_prompt_contains_text(prompt, cluster_text) or
- not self._v2_prompt_repair_text_allowed(cluster_text)
- ):
- continue
- cluster_item = dict(item)
- cluster_item["text"] = cluster_text
- selected.append(cluster_item)
- selected_texts.add(cluster_text)
- seen_axes |= item["axes"]
- if len(selected) >= 10:
- break
- if len(selected) >= 10:
- break
- if item["text"] in selected_texts:
- continue
- selected.append(item)
- selected_texts.add(item["text"])
- seen_axes |= item["axes"]
- if len(selected) >= 10 or (len(selected) >= 4 and missing_axes <= seen_axes):
- break
-
- if not selected:
- fallback_axes = set(V2_FEEDBACK_AXES) - missing_axes
- if allow_axis_fallback and fallback_axes:
- fallback_feedback = dict(axis_feedback or {})
- fallback_feedback["missing_axes"] = self._v2_order_axes(fallback_axes)
- fallback_prompt, fallback_status, fallback_candidates = self._v2_repair_prompt_for_missing_axes(
- prompt,
- current_phrases,
- global_state,
- previous_run,
- fallback_feedback,
- intent_phrases=intent_phrases,
- intent_family_slot=intent_family_slot,
- allow_axis_fallback=False,
- apply=apply,
- )
- if fallback_candidates:
- return (
- fallback_prompt,
- "Prompt repair: no candidates for requested axes "
- f"({', '.join(self._v2_order_axes(missing_axes))}); "
- f"showing other-axis repair candidates. {fallback_status}",
- fallback_candidates,
- )
- return (
- prompt,
- "Prompt repair: no stored candidates for requested axes "
- f"({', '.join(self._v2_order_axes(missing_axes))}); "
- "no other-axis repair candidates either.",
- [],
- )
- return prompt, f"Prompt repair: no stored candidates for missing {', '.join(self._v2_order_axes(missing_axes))}.", []
-
- additions = [item["text"] for item in selected]
- source_counts = {}
- for item in selected:
- source_counts[item["source"]] = int(source_counts.get(item["source"], 0)) + 1
- source_summary = ", ".join(f"{source}:{count}" for source, count in sorted(source_counts.items()))
- axes_label = ', '.join(self._v2_order_axes(missing_axes))
- if not apply:
- return (
- prompt,
- f"Prompt repair: {len(additions)} suggestion(s) for missing {axes_label} ({source_summary}): {', '.join(additions)}.",
- selected,
- )
- repaired = f"{prompt}, {', '.join(additions)}" if str(prompt or "").strip() else ", ".join(additions)
- return (
- repaired,
- f"Prompt repair: added {len(additions)} phrase(s) for missing {axes_label} ({source_summary}): {', '.join(additions)}.",
- selected,
- )
-
- def _v2_apply_perfect_repair_phrases(
- self,
- prompt,
- intent_family_slot,
- intent_prompt="",
- intent_phrases=None,
- clip=None,
- encode_cache=None,
- ):
- if not isinstance(intent_family_slot, dict):
- return prompt, "Perfect repairs: none.", []
- repairs = intent_family_slot.get("perfect_repairs", {})
- if not isinstance(repairs, dict) or not repairs:
- return prompt, "Perfect repairs: none.", []
- rejected = intent_family_slot.get("rejected_repairs", {})
- rejected = rejected if isinstance(rejected, dict) else {}
- additions = []
- for entry in sorted(
- repairs.values(),
- key=lambda item: (
- int(item.get("count", 0)) if isinstance(item, dict) else 0,
- int(item.get("last_seen_iter", 0)) if isinstance(item, dict) else 0,
- ),
- reverse=True,
- ):
- if not isinstance(entry, dict):
- continue
- text = self._v2_clean_phrase_text(entry.get("text", ""))
- if (
- not text or
- self._v2_prompt_contains_text(prompt, text) or
- not self._v2_prompt_repair_text_allowed(text) or
- not self._v2_memory_entry_matches_current_scene(
- text,
- intent_prompt or prompt,
- intent_phrases or [],
- clip=clip,
- encode_cache=encode_cache,
- )
- ):
- continue
- rejection = rejected.get(text)
- if (
- isinstance(rejection, dict) and
- int(rejection.get("count", 0) or 0) >= int(entry.get("count", 0) or 0) and
- not self._v2_phrase_represented_by(text, intent_phrases or [])
- ):
- continue
- additions.append(text)
- if len(additions) >= 6:
- break
- if not additions:
- return prompt, "Perfect repairs: already represented.", []
- repaired = f"{prompt}, {', '.join(additions)}" if str(prompt or "").strip() else ", ".join(additions)
- return (
- repaired,
- f"Perfect repairs: preserved {len(additions)} Perfect-proven phrase(s): {', '.join(additions)}.",
- [{"text": text, "source": "perfect_repair", "action": "added"} for text in additions],
- )
-
def _v2_serializable_repair_candidates(self, candidates):
serializable = []
for item in candidates or []:
diff --git a/tests/test_v2_category_alignment.py b/tests/test_v2_category_alignment.py
index 31ee5e6b..badfbb0b 100644
--- a/tests/test_v2_category_alignment.py
+++ b/tests/test_v2_category_alignment.py
@@ -259,62 +259,6 @@ def test_wrong_appearance_rating_is_available():
assert profile["wrong_categories"] == ["appearance", "subject", "environment"]
-def test_prompt_repair_blocks_appearance_subject_and_environment():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {
- "white tights": {
- "text": "white tights",
- "primary": "appearance",
- "effective_category_scores": refiner._v2_heuristic_scores("white tights"),
- "wanted_axes": {"details": 3},
- "score": 6.0,
- "liked_count": 4,
- },
- "detailed background": {
- "text": "detailed background",
- "primary": "environment",
- "effective_category_scores": refiner._v2_heuristic_scores("detailed background"),
- "wanted_axes": {"details": 3},
- "score": 6.0,
- "liked_count": 4,
- },
- "female character": {
- "text": "female character",
- "primary": "subject",
- "effective_category_scores": refiner._v2_heuristic_scores("female character"),
- "wanted_axes": {"details": 3},
- "score": 6.0,
- "liked_count": 4,
- },
- "tiny smoke curls": {
- "text": "tiny smoke curls",
- "primary": "details",
- "effective_category_scores": refiner._v2_heuristic_scores("tiny smoke curls"),
- "wanted_axes": {"details": 3},
- "score": 4.0,
- "liked_count": 2,
- },
- }
- }
- profile = normalize_refiner_v2_rating("Missing details")
- feedback = refiner._v2_axis_feedback(profile, None)
-
- repaired, _, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "person smoking",
- refiner._v2_classify_phrases(None, prompt_items(refiner, ["person smoking"]), global_state),
- global_state,
- None,
- feedback,
- )
-
- assert "tiny smoke curls" in repaired
- assert "white tights" not in repaired
- assert "detailed background" not in repaired
- assert "female character" not in repaired
- assert all(candidate["text"] != "white tights" for candidate in candidates)
-
-
def test_scene_builder_user_category_overrides_refiner_classification():
refiner = FunPackVideoRefinerV2()
scene_db = {
@@ -428,26 +372,6 @@ def test_lucky_composition_gates_unrelated_old_action_by_current_intent():
assert "Lucky: on" in status
-def test_perfect_repair_does_not_inject_after_scene_change():
- refiner = FunPackVideoRefinerV2()
- slot = {
- "perfect_repairs": {
- "dancing in rain": {"text": "dancing in rain", "axes": {"action": 1}, "count": 3}
- }
- }
-
- repaired, status, adjustments = refiner._v2_apply_perfect_repair_phrases(
- "walking on beach",
- slot,
- intent_prompt="walking on beach",
- intent_phrases=prompt_phrases(refiner, "walking on beach"),
- )
-
- assert repaired == "walking on beach"
- assert adjustments == []
- assert "already represented" in status
-
-
def test_intent_alignment_learns_missing_original_intent_from_enhancer_variant():
refiner = FunPackVideoRefinerV2()
global_state = {"phrase_memory": {}, "intent_alignment_memory": {}}
@@ -616,96 +540,6 @@ def test_intent_alignment_omits_bad_token_in_new_enhancer_phrase():
]
-def test_repeated_intent_family_phrase_becomes_conservative_repair_candidate():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {},
- "intent_family_memory": {},
- "perfect_anchors": {},
- "variant_evidence": {},
- "intent_preference_phrases": {},
- "conditioning_deltas": {},
- }
- intent_prompt = "yellow car riding down the road"
- enhanced_prompt = "yellow car in a desert"
- profile = normalize_refiner_v2_rating("Missing action")
- feedback = refiner._v2_axis_feedback(profile, None)
-
- for iteration in range(2):
- refiner._v2_update_intent_family_memory(
- global_state,
- {
- "prompt": enhanced_prompt,
- "phrases": prompt_phrases(refiner, enhanced_prompt, global_state),
- "intent_prompt": intent_prompt,
- "intent_phrases": prompt_phrases(refiner, intent_prompt, global_state),
- },
- profile,
- iteration + 1,
- feedback,
- )
-
- current_intent = "yellow car down the road"
- _, family_slot, _ = refiner._v2_intent_family_slot(global_state, current_intent, create=False)
- repaired, status, candidates = refiner._v2_repair_prompt_for_missing_axes(
- enhanced_prompt,
- prompt_phrases(refiner, enhanced_prompt, global_state),
- global_state,
- None,
- feedback,
- intent_phrases=prompt_phrases(refiner, current_intent, global_state),
- intent_family_slot=family_slot,
- )
-
- assert "yellow car riding down the road" in repaired
- assert "intent_preference" in status
- assert any(candidate["source"] == "intent_preference" for candidate in candidates)
-
-
-def test_repeated_intent_preference_does_not_repair_unrelated_family():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {},
- "intent_family_memory": {},
- "perfect_anchors": {},
- "variant_evidence": {},
- "intent_preference_phrases": {},
- "conditioning_deltas": {},
- }
- yellow_intent = "yellow car riding down the road"
- dragon_intent = "dragon sleeping in a cave"
- profile = normalize_refiner_v2_rating("Missing action")
- feedback = refiner._v2_axis_feedback(profile, None)
-
- for iteration in range(2):
- refiner._v2_update_intent_family_memory(
- global_state,
- {
- "prompt": "yellow car in a desert",
- "phrases": prompt_phrases(refiner, "yellow car in a desert", global_state),
- "intent_prompt": yellow_intent,
- "intent_phrases": prompt_phrases(refiner, yellow_intent, global_state),
- },
- profile,
- iteration + 1,
- feedback,
- )
-
- _, dragon_slot, _ = refiner._v2_intent_family_slot(global_state, dragon_intent, create=True)
- repaired, _, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "dragon in a cave",
- prompt_phrases(refiner, "dragon in a cave", global_state),
- global_state,
- None,
- feedback,
- intent_phrases=prompt_phrases(refiner, dragon_intent, global_state),
- intent_family_slot=dragon_slot,
- )
-
- assert "yellow car riding down the road" not in repaired
- assert all(candidate["text"] != "yellow car riding down the road" for candidate in candidates)
-
-
def test_active_repair_axes_persist_until_perfect():
refiner = FunPackVideoRefinerV2()
global_state = {"active_repair_axes": []}
@@ -731,78 +565,6 @@ def test_active_repair_axes_persist_until_perfect():
assert "cleared by Perfect" in status
-def test_perfect_rating_preserves_successful_repair_phrases():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {},
- "intent_family_memory": {},
- "perfect_anchors": {},
- "variant_evidence": {},
- "intent_preference_phrases": {},
- "conditioning_deltas": {},
- }
- intent_prompt = "person smoking"
- profile = normalize_refiner_v2_rating("Perfect")
- feedback = refiner._v2_axis_feedback(profile, None)
- source = tensor_to_serializable(torch.zeros(1, 3, 2))
- refined = tensor_to_serializable(torch.ones(1, 3, 2))
-
- refiner._v2_update_intent_family_memory(
- global_state,
- {
- "prompt": "person smoking",
- "encoded_prompt": "person smoking, tiny smoke curls",
- "phrases": prompt_phrases(refiner, "person smoking", global_state),
- "intent_prompt": intent_prompt,
- "intent_phrases": prompt_phrases(refiner, intent_prompt, global_state),
- "source_conditioning": source,
- "conditioning": refined,
- "repair_candidates": [{"text": "tiny smoke curls", "axes": ["details"], "score": 2.0, "source": "memory"}],
- },
- profile,
- 1,
- feedback,
- )
- _, slot, _ = refiner._v2_intent_family_slot(global_state, intent_prompt, create=False)
- repaired, status, adjustments = refiner._v2_apply_perfect_repair_phrases("person smoking", slot)
-
- assert "tiny smoke curls" in repaired
- assert "Perfect-proven" in status
- assert adjustments == [{"text": "tiny smoke curls", "source": "perfect_repair", "action": "added"}]
-
-
-def test_prompt_repair_falls_back_to_any_axis_candidates_when_requested_axis_is_empty():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {
- "portrait quality": {
- "text": "portrait quality",
- "primary": "quality",
- "effective_category_scores": refiner._v2_heuristic_scores("portrait quality"),
- "wanted_axes": {"quality": 3},
- "score": 4.0,
- "liked_count": 2,
- "category_evidence_count": 2,
- }
- }
- }
- profile = normalize_refiner_v2_rating("Missing action")
- feedback = refiner._v2_axis_feedback(profile, None)
-
- repaired, status, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "static portrait",
- prompt_phrases(refiner, "static portrait", global_state),
- global_state,
- None,
- feedback,
- )
-
- assert "portrait quality" in repaired
- assert "showing other-axis repair candidates" in status
- assert candidates[0]["text"] == "portrait quality"
- assert candidates[0]["source"] == "memory"
-
-
def test_intent_family_perfect_anchor_keeps_loved_variant():
refiner = FunPackVideoRefinerV2()
global_state = {
@@ -1899,258 +1661,6 @@ def test_normal_previous_run_can_train_before_current_refusal_is_discarded(tmp_p
assert "sorry, but i can't assist with that request." not in state["prompt_histories"]
-def test_liked_action_detail_context_clusters_repair_missing_axes():
- refiner = FunPackVideoRefinerV2()
- global_state = {"phrase_memory": {}, "preferred_context_memory": {}}
- rich_prompt = "woman walking reaching hand tiny particles rain reflections"
- liked_phrases = refiner._v2_classify_phrases(
- None,
- prompt_items(refiner, [
- "woman",
- "walking",
- "reaching hand",
- "tiny particles",
- "rain reflections",
- ]),
- global_state,
- )
- profile = normalize_refiner_v2_rating("Perfect")
- feedback = refiner._v2_axis_feedback(profile, None)
-
- refiner._v2_update_phrase_memory(
- global_state,
- {"prompt": rich_prompt, "phrases": liked_phrases},
- profile,
- 1,
- feedback,
- )
- assert "Preferred context stored" in refiner._v2_update_preferred_context_memory(
- global_state,
- {"phrases": liked_phrases},
- profile,
- 2,
- feedback,
- )
-
- missing_profile = normalize_refiner_v2_rating("Missing details + action")
- missing_feedback = refiner._v2_axis_feedback(missing_profile, None)
- current_phrases = refiner._v2_classify_phrases(
- None,
- prompt_items(refiner, ["woman", "rain"]),
- global_state,
- )
- intent_phrases = refiner._v2_classify_phrases(
- None,
- refiner._ordered_prompt_phrases(rich_prompt),
- global_state,
- )
-
- repaired, status, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "woman, rain",
- current_phrases,
- global_state,
- None,
- missing_feedback,
- intent_phrases=intent_phrases,
- )
-
- assert "walking" in repaired
- assert "tiny particles" in repaired
- assert "intent" in status or "preferred_context" in status
- assert len(candidates) >= 2
-
-
-def test_prompt_repair_does_not_pull_unrequested_memory_for_any_missing_axis():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {
- "walking": {
- "text": "walking",
- "primary": "action",
- "effective_category_scores": refiner._v2_heuristic_scores("walking"),
- "wanted_axes": {"action": 5},
- "score": 5.0,
- "liked_count": 5,
- },
- "tiny particles": {
- "text": "tiny particles",
- "primary": "details",
- "effective_category_scores": refiner._v2_heuristic_scores("tiny particles"),
- "wanted_axes": {"details": 5},
- "score": 5.0,
- "liked_count": 5,
- },
- "cinematic lighting": {
- "text": "cinematic lighting",
- "primary": "style",
- "effective_category_scores": refiner._v2_heuristic_scores("cinematic lighting"),
- "wanted_axes": {"quality": 5},
- "score": 5.0,
- "liked_count": 5,
- },
- "glass reflections": {
- "text": "glass reflections",
- "primary": "details",
- "effective_category_scores": refiner._v2_heuristic_scores("glass reflections"),
- "wanted_axes": {"details": 2},
- "score": 2.0,
- "liked_count": 1,
- },
- },
- }
- profile = normalize_refiner_v2_rating("Awful")
- feedback = refiner._v2_axis_feedback(profile, None)
- current_phrases = refiner._v2_classify_phrases(
- None,
- prompt_items(refiner, ["woman", "glass"]),
- global_state,
- )
-
- repaired, _, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "woman, glass",
- current_phrases,
- global_state,
- None,
- feedback,
- )
-
- candidate_texts = {candidate["text"] for candidate in candidates}
- assert "glass reflections" in repaired
- assert "walking" not in repaired
- assert "tiny particles" not in repaired
- assert "cinematic lighting" not in repaired
- assert "walking" not in candidate_texts
- assert "tiny particles" not in candidate_texts
- assert "cinematic lighting" not in candidate_texts
-
-
-def test_prompt_repair_treats_same_word_with_different_neighbours_as_different():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {
- "turning head": {
- "text": "turning head",
- "primary": "action",
- "effective_category_scores": refiner._v2_heuristic_scores("turning head"),
- "wanted_axes": {"action": 4},
- "score": 4.0,
- "liked_count": 4,
- "context_senses": {
- "mid|eyes,portrait": {
- "context": {
- "anchor_words": ["head", "turning"],
- "context_words": ["portrait", "eyes"],
- "position_bucket": "mid",
- "window": 3,
- },
- "category_weights": refiner._v2_category_template(0.0),
- "effective_category_scores": refiner._v2_heuristic_scores("turning head"),
- "category_evidence_count": 4,
- "occurrence_count": 4,
- },
- },
- },
- },
- }
- profile = normalize_refiner_v2_rating("Missing action")
- feedback = refiner._v2_axis_feedback(profile, None)
- current_phrases = refiner._v2_classify_phrases(
- None,
- prompt_items(refiner, ["car", "turning wheel", "street"]),
- global_state,
- )
-
- repaired, _, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "car, turning wheel, street",
- current_phrases,
- global_state,
- None,
- feedback,
- )
-
- assert "turning head" not in repaired
- assert all(candidate["text"] != "turning head" for candidate in candidates)
-
-
-def test_vague_user_intent_lets_enhanced_positive_prompt_anchor_repair():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {
- "walking slowly": {
- "text": "walking slowly",
- "primary": "action",
- "effective_category_scores": refiner._v2_heuristic_scores("walking slowly"),
- "wanted_axes": {"action": 3},
- "score": 3.0,
- "liked_count": 3,
- },
- },
- }
- assert refiner._v2_user_intent_prompt_is_vague("Figure it out")
- assert not refiner._v2_user_intent_prompt_is_vague("person walking")
-
- profile = normalize_refiner_v2_rating("Missing action")
- feedback = refiner._v2_axis_feedback(profile, None)
- current_phrases = refiner._v2_classify_phrases(
- None,
- prompt_items(refiner, ["woman", "walking through glass hallway"]),
- global_state,
- )
-
- repaired, _, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "woman, walking through glass hallway",
- current_phrases,
- global_state,
- None,
- feedback,
- intent_phrases=[],
- )
-
- assert "walking slowly" in repaired
- assert any(candidate["text"] == "walking slowly" for candidate in candidates)
-
-
-def test_complex_user_intent_ranks_before_matching_preferred_memory():
- refiner = FunPackVideoRefinerV2()
- global_state = {
- "phrase_memory": {},
- "preferred_context_memory": {
- "reach-memory": {
- "anchor": "reaching hand slowly",
- "axes": {"action": 4},
- "context": {},
- "phrases": ["reaching hand slowly"],
- "score": 10.0,
- "liked_count": 8,
- },
- },
- }
- profile = normalize_refiner_v2_rating("Missing action")
- feedback = refiner._v2_axis_feedback(profile, None)
- current_phrases = refiner._v2_classify_phrases(
- None,
- prompt_items(refiner, ["woman"]),
- global_state,
- )
- intent_phrases = refiner._v2_classify_phrases(
- None,
- refiner._ordered_prompt_phrases("woman reaching hand toward glass"),
- global_state,
- )
-
- repaired, _, candidates = refiner._v2_repair_prompt_for_missing_axes(
- "woman",
- current_phrases,
- global_state,
- None,
- feedback,
- intent_phrases=intent_phrases,
- )
-
- assert "reaching hand toward glass" in repaired
- assert candidates[0]["source"] == "intent"
-
-
def test_phrase_clusters_train_more_strongly_than_ngrams_and_tokens():
refiner = FunPackVideoRefinerV2()
global_state = {"phrase_memory": {}, "preferred_context_memory": {}}
From 61e709741637f89eca246b35fa83bce810a3069b Mon Sep 17 00:00:00 2001
From: digital-garbage <133010426+digital-garbage@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:27:15 +0300
Subject: [PATCH 3/7] README: document cutting_edge branch under Dev and
Cutting Edge branches
Co-Authored-By: Claude Opus 4.8
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 446c35b7..23de20d0 100644
--- a/README.md
+++ b/README.md
@@ -8,12 +8,14 @@ A set of ComfyUI nodes for experimenting with video generation workflows based o
-## Dev Branch
+## Dev and Cutting Edge branches
The `dev` branch is intended for testing unfinished changes, implementing new logic and basically, flipping everything just because I can. It can be broken, renamed, or changed without warning.
Use only `main` if you want the most stable version of this node pack. Bug reports based on `dev` version will be ignored.
+Cutting edge branch is designed for testing the most advanced yet most breaking changes and features. I do not recommend using cutting_edge branch unless it's strictly necessary for your workflow (e.g. a feature you wanted got a rework or else) - any sort of stability is not guaranteed.
+
## Version 3.0 — Cutting Room and development direction
FunPack **3.0** introduces the **Cutting Room** — a dedicated montage editor at `/funpack/movie` inside ComfyUI. It is the primary surface for building multi-scene projects, previewing on a real timeline, and driving FunPack Studio + the LTXAV Scene Chain Sampler without wiring a graph by hand.
From df011b927f5e0fbb21ea6bec71ba2f2cdff99da2 Mon Sep 17 00:00:00 2001
From: digital-garbage <133010426+digital-garbage@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:44:55 +0300
Subject: [PATCH 4/7] =?UTF-8?q?Stage=202=20Part=20B:=20"Wrong=20appearance?=
=?UTF-8?q?"=20=E2=86=92=20consistency=20anchor?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reworks the "Wrong appearance" rating from a (now-removed) prompt-repair
signal into a conditioning-level consistency anchor (option b). "Wrong
appearance" now means "character consistency wasn't preserved" and steers
the next generation back toward the established character.
Capture (_v2_update_appearance_anchor, wired into both the single-run
learning path and the shared per-scene suite _v2_learn_scene_into_state):
- good rating (like/nailed_it/loved_it/loved) → store the rated run's
conditioning as the per-key "blessed appearance" (overwrite = last good)
- wrong_appearance → store the rejected run's conditioning as the "drift"
global_state is already per-refinement-key, so both live with that key.
Apply (_v2_apply_conditioning_memory_impl, after the intent-family delta):
when the user rated the previous gen wrong_appearance, gently pull this gen
toward the blessed appearance (lerp 0.05) and repel from the drift (0.04),
reusing _v2_apply_conditioning_payload / _v2_repel_conditioning_payload. No
ramping; the existing 7.5% delta cap bounds the net effect. No-op when
nothing is stored or shapes mismatch.
Drops the dead wrong_categories field (zero consumers after Part A) and
keeps skip_value_function (the anchor is a conditioning nudge, not a reward).
Tests: 7 new unit tests (storage + overwrite + skip + pull/repel + idle +
non-trigger); updated test_wrong_appearance_rating_is_available for the new
semantics. Zero new failures vs HEAD (the 10/31 remaining are pre-existing
env failures from missing server/comfy modules).
Co-Authored-By: Claude Opus 4.8
---
conditioning.py | 80 +++++++++++++++--
tests/test_v2_category_alignment.py | 134 +++++++++++++++++++++++++++-
2 files changed, 207 insertions(+), 7 deletions(-)
diff --git a/conditioning.py b/conditioning.py
index 3b130895..ab067d53 100644
--- a/conditioning.py
+++ b/conditioning.py
@@ -6113,21 +6113,24 @@ V2_RATING_PROFILES = {
"Missing action": {"key": "missing_action", "reward": 0.05, "level": 5, "missing_axes": ["action"]},
"Missing quality": {"key": "missing_quality", "reward": -0.30, "level": 4, "missing_axes": ["quality"]},
"Missing details + action": {"key": "missing_details_action", "reward": -0.10, "level": 3, "missing_axes": ["details", "action"]},
- # Wrong-* are prompt-REPAIR signals ("good gen, but the words/identity were off"), not quality
- # rewards. `skip_value_function` keeps them out of value-function + Absolute-taste reward
- # training (where a 0.0/low reward on a visually-good gen poisons the learned quality landscape
- # and the ascent direction → drift/wrong-character). They STILL drive prompt repair and the
- # relative direction/category memory via their missing_axes/wrong_axes/wrong_categories.
+ # Wrong-* are quality-neutral signals ("good gen, but the words/identity were off"), not
+ # quality rewards. `skip_value_function` keeps them out of value-function + Absolute-taste
+ # reward training (where a 0.0/low reward on a visually-good gen poisons the learned quality
+ # landscape and the ascent direction → drift/wrong-character). They STILL drive the relative
+ # direction/category memory via their missing_axes/wrong_axes.
"Wrong details": {"key": "wrong_details", "reward": 0.20, "level": 5, "missing_axes": ["details"], "wrong_axes": ["details"], "skip_value_function": True},
"Wrong action": {"key": "wrong_action", "reward": -0.10, "level": 4, "missing_axes": ["action"], "wrong_axes": ["action"], "skip_value_function": True},
"Wrong action + quality": {"key": "wrong_action_quality", "reward": -0.40, "level": 3, "missing_axes": ["quality"], "wrong_axes": ["action"], "skip_value_function": True},
"Wrong details + action": {"key": "wrong_details_action", "reward": 0.00, "level": 3, "missing_axes": ["details", "action"], "wrong_axes": ["details", "action"], "skip_value_function": True},
+ # "Wrong appearance" = character consistency wasn't preserved. Drives the consistency anchor
+ # (_v2_update_appearance_anchor / _v2_apply_conditioning_memory_impl): pull the next gen toward
+ # the last good-rated conditioning for this key and repel from the rejected drift. Not a reward
+ # (skip_value_function) — it's a conditioning nudge, not a quality score.
"Wrong appearance": {
"key": "wrong_appearance",
"reward": 0.0,
"level": 4,
"missing_axes": [],
- "wrong_categories": ["appearance", "subject", "environment"],
"skip_value_function": True,
},
"Missing details + quality": {"key": "missing_details_quality", "reward": -0.40, "level": 2, "missing_axes": ["details", "quality"]},
@@ -6858,6 +6861,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
seed_output_connected=bool(seed_output_connected),
)
self._v2_update_intent_family_memory(target_global, scene_run, profile, iter_num, axis_feedback)
+ self._v2_update_appearance_anchor(target_global, scene_run, profile, iter_num)
self._v2_update_negative_prompt_memory(target_global, scene_run, profile, axis_feedback)
self._v2_update_intent_alignment_memory(target_global, scene_run, profile, iter_num, axis_feedback)
self._v2_update_conditioning_memory(target_global, scene_run, profile, axis_feedback)
@@ -8365,6 +8369,42 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
slot["count"] = 1
return True
+ def _v2_update_appearance_anchor(self, global_state, previous_run, rating_profile, iter_num):
+ """Consistency anchor for the "Wrong appearance" rating. global_state is already
+ per-refinement-key, so these live with that key automatically.
+
+ On a good rating, remember the rated run's conditioning as the "blessed appearance"
+ (the last good character). On a Wrong-appearance rating, remember the rejected run's
+ conditioning as the "drift" to repel from. _v2_apply_conditioning_memory_impl pulls the
+ next gen toward the blessed appearance and away from the drift when the user rated the
+ previous gen Wrong appearance."""
+ if not isinstance(global_state, dict) or not isinstance(previous_run, dict):
+ return "Appearance anchor: no update."
+ if rating_profile.get("skip_learning"):
+ return "Appearance anchor: no update."
+ conditioning = previous_run.get("conditioning")
+ if not isinstance(conditioning, dict):
+ return "Appearance anchor: run carried no conditioning."
+ key = rating_profile.get("key", "")
+ is_good = key in {"like", "nailed_it", "loved_it"} or bool(rating_profile.get("loved_modifier"))
+ if is_good:
+ existing = global_state.get("appearance_anchor") or {}
+ global_state["appearance_anchor"] = {
+ "conditioning": conditioning,
+ "prompt": self._v2_prompt_key(previous_run.get("prompt", "")),
+ "count": int(existing.get("count", 0)) + 1,
+ "last_seen_iter": int(iter_num),
+ }
+ return "Appearance anchor: blessed appearance stored."
+ if key == "wrong_appearance":
+ global_state["appearance_drift"] = {
+ "conditioning": conditioning,
+ "prompt": self._v2_prompt_key(previous_run.get("prompt", "")),
+ "last_seen_iter": int(iter_num),
+ }
+ return "Appearance anchor: drift stored (repel on next gen)."
+ return "Appearance anchor: no update."
+
def _v2_update_intent_family_memory(self, global_state, last_run, rating_profile, iter_num, axis_feedback=None):
if not isinstance(global_state, dict) or not isinstance(last_run, dict) or rating_profile.get("skip_learning"):
return "Intent family: no learning update.", ""
@@ -9971,6 +10011,25 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
mixed, family_delta_status = self._v2_apply_intent_family_delta(mixed, intent_family_slot, strength)
+ # --- Consistency anchor (Wrong appearance) ---
+ # "Wrong appearance" means the established character drifted. Pull this run gently back
+ # toward the last good-rated conditioning for this key (the "blessed appearance") and
+ # repel from the just-rejected drifted look. Gentle fixed strengths; the global 7.5%
+ # delta cap below bounds the net effect. No-op when nothing is stored or shapes mismatch.
+ appearance_status = "appearance-anchor idle"
+ if rating_profile.get("key") == "wrong_appearance":
+ anchor = global_state.get("appearance_anchor") or {}
+ drift = global_state.get("appearance_drift") or {}
+ anchor_actions = []
+ if self._v2_shape_compatible(anchor.get("conditioning"), mixed):
+ mixed = self._v2_apply_conditioning_payload(mixed, anchor.get("conditioning"), 0.05)
+ anchor_actions.append("pull→blessed")
+ if self._v2_shape_compatible(drift.get("conditioning"), mixed):
+ mixed = self._v2_repel_conditioning_payload(mixed, drift.get("conditioning"), 0.04)
+ anchor_actions.append("repel←drift")
+ if anchor_actions:
+ appearance_status = "appearance-anchor: " + " + ".join(anchor_actions)
+
axis_memory = global_state.get("axis_conditioning_memory", {})
axis_actions = []
missing_axes = set(axis_feedback.get("missing_axes", []))
@@ -10096,6 +10155,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
f" Bad conditioning: {'direction' if bad_count >= 3 else f'lerp fallback ({bad_count}/3 runs)'}\n"
f" Axis adjustments: {axis_str}\n"
f" {family_delta_status}\n"
+ f" {appearance_status}\n"
f" Total delta clamped to 7.5% of conditioning norm"
)
@@ -11610,6 +11670,8 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
global_state.setdefault("intent_alignment_memory", {})
global_state.setdefault("intent_family_memory", {})
global_state.setdefault("perfect_anchors", {})
+ global_state.setdefault("appearance_anchor", {})
+ global_state.setdefault("appearance_drift", {})
global_state.setdefault("variant_evidence", {})
global_state.setdefault("intent_preference_phrases", {})
global_state.setdefault("conditioning_deltas", {})
@@ -11711,6 +11773,12 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
int(global_state.get("total_iterations", 0)) + 1,
axis_feedback,
)
+ self._v2_update_appearance_anchor(
+ global_state,
+ previous_run,
+ learning_profile,
+ int(global_state.get("total_iterations", 0)) + 1,
+ )
negative_memory_status = self._v2_update_negative_prompt_memory(
global_state,
previous_run,
diff --git a/tests/test_v2_category_alignment.py b/tests/test_v2_category_alignment.py
index badfbb0b..5dd62210 100644
--- a/tests/test_v2_category_alignment.py
+++ b/tests/test_v2_category_alignment.py
@@ -256,7 +256,10 @@ def test_wrong_appearance_rating_is_available():
assert "Wrong appearance" in V2_RATING_LABELS
assert profile["key"] == "wrong_appearance"
- assert profile["wrong_categories"] == ["appearance", "subject", "environment"]
+ # Wrong appearance now drives the consistency anchor, not prompt repair (the dead
+ # wrong_categories field was dropped in Stage 2 Part B). It stays quality-neutral.
+ assert "wrong_categories" not in profile
+ assert profile["skip_value_function"] is True
def test_scene_builder_user_category_overrides_refiner_classification():
@@ -2067,3 +2070,132 @@ def test_split_by_transitions_uses_provided_scene_seeds(tmp_path):
assert [item[1]["funpack_scene_seed"] for item in cond] == [900, 901]
assert all(item[1]["funpack_seed_source"] == "successful seed memory" for item in cond)
+
+
+# --- Stage 2 Part B: "Wrong appearance" consistency anchor ---
+
+def _mean_cosine(a, b):
+ a = a.reshape(-1, a.shape[-1]).float()
+ b = b.reshape(-1, b.shape[-1]).float()
+ a = a / a.norm(dim=-1, keepdim=True).clamp_min(1e-8)
+ b = b / b.norm(dim=-1, keepdim=True).clamp_min(1e-8)
+ return float((a * b).sum(dim=-1).mean().item())
+
+
+def test_appearance_anchor_stores_blessed_on_good_rating():
+ refiner = FunPackVideoRefinerV2()
+ global_state = {}
+ payload = tensor_to_serializable(torch.randn(1, 6, 8))
+ previous_run = {"conditioning": payload, "prompt": "woman in red dress"}
+
+ status = refiner._v2_update_appearance_anchor(
+ global_state, previous_run, normalize_refiner_v2_rating("Perfect"), 3
+ )
+
+ assert "blessed" in status.lower()
+ assert global_state["appearance_anchor"]["conditioning"] is payload
+ assert global_state["appearance_anchor"]["count"] == 1
+ # A good rating should NOT populate the drift slot.
+ assert "appearance_drift" not in global_state
+
+
+def test_appearance_anchor_stores_drift_on_wrong_appearance():
+ refiner = FunPackVideoRefinerV2()
+ global_state = {}
+ payload = tensor_to_serializable(torch.randn(1, 6, 8))
+ previous_run = {"conditioning": payload, "prompt": "woman in red dress"}
+
+ refiner._v2_update_appearance_anchor(
+ global_state, previous_run, normalize_refiner_v2_rating("Wrong appearance"), 4
+ )
+
+ assert global_state["appearance_drift"]["conditioning"] is payload
+ # Wrong appearance must not overwrite/seed the blessed anchor.
+ assert "appearance_anchor" not in global_state
+
+
+def test_good_rating_overwrites_blessed_anchor():
+ refiner = FunPackVideoRefinerV2()
+ global_state = {}
+ first = tensor_to_serializable(torch.randn(1, 6, 8))
+ second = tensor_to_serializable(torch.randn(1, 6, 8))
+
+ refiner._v2_update_appearance_anchor(
+ global_state, {"conditioning": first, "prompt": "a"}, normalize_refiner_v2_rating("Perfect"), 1
+ )
+ refiner._v2_update_appearance_anchor(
+ global_state, {"conditioning": second, "prompt": "b"}, normalize_refiner_v2_rating("Nailed it"), 2
+ )
+
+ assert global_state["appearance_anchor"]["conditioning"] is second
+ assert global_state["appearance_anchor"]["count"] == 2
+
+
+def test_appearance_anchor_skips_when_no_conditioning_or_skip_learning():
+ refiner = FunPackVideoRefinerV2()
+ global_state = {}
+ # No conditioning payload on the run.
+ refiner._v2_update_appearance_anchor(
+ global_state, {"prompt": "x"}, normalize_refiner_v2_rating("Perfect"), 1
+ )
+ assert "appearance_anchor" not in global_state
+ # skip_learning rating (e.g. -Just forget it-) never stores.
+ payload = tensor_to_serializable(torch.randn(1, 6, 8))
+ refiner._v2_update_appearance_anchor(
+ global_state, {"conditioning": payload}, normalize_refiner_v2_rating("-Just forget it-"), 1
+ )
+ assert "appearance_anchor" not in global_state
+
+
+def test_wrong_appearance_pulls_toward_blessed_and_repels_drift():
+ refiner = FunPackVideoRefinerV2()
+ torch.manual_seed(0)
+ original = torch.randn(1, 6, 8)
+ blessed = torch.randn(1, 6, 8)
+ drift = torch.randn(1, 6, 8)
+ global_state = {
+ "appearance_anchor": {"conditioning": tensor_to_serializable(blessed)},
+ "appearance_drift": {"conditioning": tensor_to_serializable(drift)},
+ }
+
+ refined, status = refiner._v2_apply_conditioning_memory(
+ original.clone(), global_state, normalize_refiner_v2_rating("Wrong appearance")
+ )
+
+ assert "appearance-anchor: pull→blessed + repel←drift" in status
+ # Output moved toward the blessed appearance and away from the drift (direction).
+ assert _mean_cosine(refined, blessed) > _mean_cosine(original, blessed)
+ assert _mean_cosine(refined, drift) < _mean_cosine(original, drift)
+ # Gentle: per-token norm is preserved and the net change is small.
+ assert torch.allclose(
+ refined.norm(dim=-1), original.norm(dim=-1), atol=1e-3
+ )
+ assert (refined - original).norm() / original.norm() < 0.08
+
+
+def test_appearance_anchor_idle_without_blessed():
+ refiner = FunPackVideoRefinerV2()
+ torch.manual_seed(1)
+ original = torch.randn(1, 6, 8)
+
+ refined, status = refiner._v2_apply_conditioning_memory(
+ original.clone(), {}, normalize_refiner_v2_rating("Wrong appearance")
+ )
+
+ assert "appearance-anchor idle" in status
+ assert torch.allclose(refined, original, atol=1e-4)
+
+
+def test_appearance_anchor_not_applied_on_non_wrong_appearance_rating():
+ refiner = FunPackVideoRefinerV2()
+ torch.manual_seed(2)
+ original = torch.randn(1, 6, 8)
+ blessed = torch.randn(1, 6, 8)
+ global_state = {"appearance_anchor": {"conditioning": tensor_to_serializable(blessed)}}
+
+ # A "Missing details" rating must not trigger the appearance anchor pull.
+ refined, status = refiner._v2_apply_conditioning_memory(
+ original.clone(), global_state, normalize_refiner_v2_rating("Missing details")
+ )
+
+ assert "appearance-anchor idle" in status
From 648a7401c6acb277f10c6e9d2960fe2a413e69a9 Mon Sep 17 00:00:00 2001
From: digital-garbage <133010426+digital-garbage@users.noreply.github.com>
Date: Mon, 15 Jun 2026 18:49:00 +0300
Subject: [PATCH 5/7] Movie Editor: don't prune a just-finished ghost on
generation end
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A scene removed mid-generation becomes a ghost; when that in-flight run
completes, _recordSegment attaches the finished media to the ghost
(pendingGen:false) and deliberately does NOT push its id to
recordedSceneIds. But _pruneGhostsAfterRegen dropped any ghost whose
afterSceneId was in recordedSceneIds — and a ghost anchored after a sibling
scene in the same run matches that, so the freshly-finished ghost was
removed from the timeline the instant generation ended.
Collect the ids of ghosts that received media this pass (completedGhostIds)
and protect them in _pruneGhostsAfterRegen. Genuinely stale ghosts (anchored
after a recorded scene, not updated this run) still get pruned as before.
Co-Authored-By: Claude Opus 4.8
---
movie_editor/frontend/store.js | 16 +++++++++++++---
1 file changed, 13 insertions(+), 3 deletions(-)
diff --git a/movie_editor/frontend/store.js b/movie_editor/frontend/store.js
index ebef0ed2..12aff897 100644
--- a/movie_editor/frontend/store.js
+++ b/movie_editor/frontend/store.js
@@ -2494,9 +2494,14 @@
// Record a completed run's output: map each of the run's scenes to the one source
// video at its cumulative in-point, so splits/deletes later play the right portions.
- function _pruneGhostsAfterRegen(recordedSceneIds) {
+ function _pruneGhostsAfterRegen(recordedSceneIds, protectedGhostIds) {
const ids = new Set(recordedSceneIds || []);
- state.sceneGhosts = (state.sceneGhosts || []).filter((g) => !ids.has(g.afterSceneId) && !ids.has(g.id));
+ const keep = new Set(protectedGhostIds || []);
+ // Drop ghosts superseded by this run's fresh renders (anchored after, or sharing id with, a
+ // recorded scene) — but never a ghost that just received its own finished media this pass.
+ state.sceneGhosts = (state.sceneGhosts || []).filter(
+ (g) => keep.has(g.id) || (!ids.has(g.afterSceneId) && !ids.has(g.id))
+ );
}
function _ghostDurationSec(g) {
@@ -2666,6 +2671,10 @@
let chainSceneIdx = 0;
let clearedRating = false;
const recordedSceneIds = [];
+ // Ghosts of scenes removed mid-generation that just received their finished media in this
+ // pass. They must survive _pruneGhostsAfterRegen — they ARE the fresh result, not the stale
+ // pre-regen render the prune is meant to drop.
+ const completedGhostIds = [];
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
if (_removedDuringGen.has(id)) {
@@ -2677,6 +2686,7 @@
const inSec = sameUnit ? sourceEnd : _recordInSec(lastChain, sourceEnd, ghost, sceneLayout, chainSceneIdx, sourceSpanSec);
ghosts[gi] = { ...ghost, media: primary, inSec, pendingGen: false };
state.sceneGhosts = ghosts;
+ completedGhostIds.push(id);
sourceEnd = inSec + _ghostDurationSec(ghost);
lastChain = ghost;
if (!sameUnit) chainSceneIdx += 1;
@@ -2697,7 +2707,7 @@
lastChain = sc;
if (!sameUnit) chainSceneIdx += 1;
}
- _pruneGhostsAfterRegen(recordedSceneIds);
+ _pruneGhostsAfterRegen(recordedSceneIds, completedGhostIds);
_syncSeparatedTracksAfterRegen(recordedSceneIds);
if (clearedRating) scheduleSaveSilent();
if (recordedSceneIds.length) {
From c2c9be6ce894115775f0620356b7f7b25053423d Mon Sep 17 00:00:00 2001
From: digital-garbage <133010426+digital-garbage@users.noreply.github.com>
Date: Mon, 15 Jun 2026 22:31:31 +0300
Subject: [PATCH 6/7] Remove the Scene Builder (Studio/Refiner) entirely
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Deletes the FunPack Scene Builder node and the Studio Scene Builder feature
— the per-key scene database (scene_db / universal_memory / saved scenes),
the Studio "Scene" tab, scene-phrase authority over classification, and the
scene-database wildcard cleanup. It was redundant: helpful on paper but
effectively unused, undrivable from the Cutting Room, and a risk to shortcuts.
Removed:
- node registration (__init__.py, funpack.py) + web/funpack_scene_builder.js
- templates.py: scene-DB storage layer (load/save/normalize/empty_scene_db,
SCENE_DB_VERSION/SCENE_STATE_FIELD/SCENE_CATEGORY_SOURCES, scene_store_path),
all scene_* helpers, resolve_scene_database_wildcards, the FunPackSceneBuilder
class, and the /funpack/scenes* routes
- conditioning.py: _v2_scene_builder_db / _v2_scene_memory_item /
_v2_apply_scene_builder_authority / _v2_scene_builder_category_for_entry /
_v2_sync_scene_builder_memory / _v2_resolve_scene_builder_wildcards (+ wildcard
helpers + _v2_scene_key), the scene_db threading through classify/learning,
the scene_category_locked phrase fields + their training branch, the
state["scene_builder"] preservation, and the Studio scene_builder prompt mode
- web/funpack_studio.js: Scene tab, SB_MODES, scene_builder settings, renderScene,
fetch/saveScene, the Session Scene-Builder section, and the Refiner banner/notice
- tests/test_scene_builder.py (whole file) + scene-builder tests in
test_v2_category_alignment.py; the SceneBuilder example workflow + its doc;
README link; Scene Builder mentions in docs/FunPackStudio.md
Kept intact: shortcuts (apply_prompt_shortcuts / shortcut DB), transitions and
the canonical split_scenes splitter, and FunPackRefinementKeyLoader. Studio now
always uses the connected positive_prompt. No new test failures (the 16/97
remaining in tests/ are pre-existing env/sampler failures); movie_editor 140 pass.
Co-Authored-By: Claude Opus 4.8
---
CHANGELOG.md | 7 +
README.md | 1 -
__init__.py | 7 +-
conditioning.py | 276 +-
docs/FunPackSceneBuilder.md | 70 -
docs/FunPackStudio.md | 14 +-
...ck_LTX23_SceneBuilder_Advisor_Updated.json | 5411 -----------------
funpack.py | 7 +-
templates.py | 847 ---
tests/test_scene_builder.py | 1048 ----
tests/test_v2_category_alignment.py | 198 -
web/funpack_scene_builder.js | 2465 --------
web/funpack_studio.js | 192 +-
13 files changed, 22 insertions(+), 10521 deletions(-)
delete mode 100644 docs/FunPackSceneBuilder.md
delete mode 100644 example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json
delete mode 100644 tests/test_scene_builder.py
delete mode 100644 web/funpack_scene_builder.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b802bcf5..20707654 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
## [Unreleased]
+### Removed
+- Removed the `FunPack Scene Builder` node and the Studio Scene Builder feature (the per-key
+ scene database, saved scenes, the Studio **Scene** tab, and scene-database wildcard cleanup).
+ It was redundant — helpful on paper but effectively unused, with no way to drive it from the
+ Cutting Room, and it risked interfering with shortcuts. Shortcuts, transition splitting, and
+ refinement keys are unchanged. Studio now always uses the connected `positive_prompt`.
+
### Fixed
- "Reset Studio session" silently wiped keys it never listed. The confirmation read the per-scene
`scene_refinement_keys` (which drops keys via its divergence fallback), while the backend reset
diff --git a/README.md b/README.md
index 23de20d0..1c212cc6 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,6 @@ Start with:
- [`docs/FunPackVideoRefinerV2QuickGuide.md`](docs/FunPackVideoRefinerV2QuickGuide.md) for a short Discord-friendly Refiner V2 guide
- [`docs/FunPackLTXAVSceneChainSampler.md`](docs/FunPackLTXAVSceneChainSampler.md) for split-scene LTXV/LTXAV continuation
- [`docs/FunPackLoraWorkflow.md`](docs/FunPackLoraWorkflow.md) for the LoRA/refiner helper workflow
-- [`docs/FunPackSceneBuilder.md`](docs/FunPackSceneBuilder.md) for scene preset workflows
Version history is available in [CHANGELOG.md](CHANGELOG.md).
diff --git a/__init__.py b/__init__.py
index 766ebea9..a7e62bc4 100644
--- a/__init__.py
+++ b/__init__.py
@@ -21,7 +21,7 @@ if __package__:
)
from .model_management import FunPackApplyLoraWeights, FunPackLoraLoader
from .samplers import FunPackHybridEuler2SSampler, FunPackDistilledFlowSampler, FunPackNormalizingSampler, FunPackLTXAVSceneChainSampler
- from .templates import FunPackRefinementKeyLoader, FunPackSceneBuilder
+ from .templates import FunPackRefinementKeyLoader
try:
from . import batch_training # noqa: F401 registers /funpack/batch/* routes
except Exception as _e:
@@ -70,10 +70,9 @@ else:
FunPackNormalizingSampler = None
FunPackLTXAVSceneChainSampler = None
try:
- from templates import FunPackRefinementKeyLoader, FunPackSceneBuilder
+ from templates import FunPackRefinementKeyLoader
except Exception:
FunPackRefinementKeyLoader = None
- FunPackSceneBuilder = None
WEB_DIRECTORY = "./web"
@@ -99,7 +98,6 @@ NODE_CLASS_MAPPINGS = {
"FunPackApplyLoraWeights": FunPackApplyLoraWeights,
"FunPackLoraLoader": FunPackLoraLoader,
"FunPackRefinementKeyLoader": FunPackRefinementKeyLoader,
- "FunPackSceneBuilder": FunPackSceneBuilder,
}
NODE_CLASS_MAPPINGS = {name: cls for name, cls in NODE_CLASS_MAPPINGS.items() if cls is not None}
@@ -125,7 +123,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"FunPackApplyLoraWeights": "FunPack Apply LoRA Weights",
"FunPackLoraLoader": "FunPack LoRA Loader",
"FunPackRefinementKeyLoader": "FunPack Refinement Key Loader",
- "FunPackSceneBuilder": "FunPack Scene Builder",
}
__all__ = [
diff --git a/conditioning.py b/conditioning.py
index ab067d53..0355b012 100644
--- a/conditioning.py
+++ b/conditioning.py
@@ -6511,19 +6511,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
except Exception as e:
print(f"[FunPackVideoRefinerV2] Absolute store cleanup failed: {e}")
if reset_session or not os.path.exists(path):
- preserved_scene_builder = None
- if reset_session and os.path.exists(path):
- try:
- with open(path, "r", encoding="utf-8") as file:
- previous = json.load(file)
- if isinstance(previous, dict) and isinstance(previous.get("scene_builder"), dict):
- preserved_scene_builder = previous["scene_builder"]
- except (json.JSONDecodeError, OSError, ValueError):
- preserved_scene_builder = None
- state = self._v2_empty_state(refinement_key)
- if preserved_scene_builder is not None:
- state["scene_builder"] = preserved_scene_builder
- return state, "fresh"
+ return self._v2_empty_state(refinement_key), "fresh"
try:
with open(path, "r", encoding="utf-8") as file:
data = json.load(file)
@@ -6811,7 +6799,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
out.append((item[0], meta))
return out
- def _v2_build_scene_learning_run(self, last_run, scene_index, clip, global_state, encode_cache, scene_db):
+ def _v2_build_scene_learning_run(self, last_run, scene_index, clip, global_state, encode_cache):
"""Slice a multi-scene last_run into one scene for per-scene Movie Editor ratings."""
if not isinstance(last_run, dict):
return None
@@ -6844,7 +6832,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
self._ordered_prompt_phrases(scene_text),
global_state,
encode_cache=encode_cache,
- scene_db=scene_db,
)
return scene_run
@@ -6905,7 +6892,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
*,
clip=None,
encode_cache=None,
- scene_db=None,
seed_output_connected=False,
refinement_key="",
):
@@ -6932,7 +6918,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
except (TypeError, ValueError):
continue
scene_run = self._v2_build_scene_learning_run(
- previous_run, scene_index, clip, global_state, encode_cache, scene_db,
+ previous_run, scene_index, clip, global_state, encode_cache,
)
if not scene_run:
lines.append(f"Scene {scene_index + 1}: skipped (no scene text in last run)")
@@ -7249,68 +7235,9 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
return bool(categories & self.AUTO_INJECT_BLOCKED_CATEGORIES)
return self._v2_primary_category_for_text(item) in self.AUTO_INJECT_BLOCKED_CATEGORIES
- def _v2_scene_key(self, text):
- text = re.sub(r"[^\w'’]+", " ", str(text or "").strip().lower(), flags=re.UNICODE)
- return re.sub(r"\s+", " ", text).strip()
-
def _v2_now_iso(self):
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
- def _v2_scene_builder_db(self, state):
- if not isinstance(state, dict):
- return {}, "Scene Builder: unavailable."
- scene_db = state.get("scene_builder")
- if not isinstance(scene_db, dict):
- return {}, "Scene Builder: unavailable."
- memory = scene_db.get("universal_memory", {})
- scenes = scene_db.get("scenes", {})
- memory_count = len(memory) if isinstance(memory, dict) else 0
- scene_count = len(scenes) if isinstance(scenes, dict) else 0
- return scene_db, f"Scene Builder: available ({memory_count} database phrase(s), {scene_count} saved scene(s))."
-
- def _v2_scene_memory_item(self, scene_db, text):
- if not isinstance(scene_db, dict):
- return None
- memory = scene_db.get("universal_memory", {})
- if not isinstance(memory, dict):
- return None
- key = self._v2_scene_key(text)
- if key and isinstance(memory.get(key), dict):
- return memory[key]
- clean = self._v2_clean_phrase_text(text)
- for item in memory.values():
- if not isinstance(item, dict):
- continue
- item_text = self._v2_clean_phrase_text(item.get("text", ""))
- if item_text and clean and self._v2_phrase_texts_match(clean, item_text):
- return item
- return None
-
- def _v2_apply_scene_builder_authority(self, item, scene_db):
- scene_item = self._v2_scene_memory_item(scene_db, item.get("text", "") if isinstance(item, dict) else item)
- if not isinstance(item, dict) or not isinstance(scene_item, dict) or not bool(scene_item.get("category_locked")):
- return False
- category = str(scene_item.get("category") or "").strip().lower()
- item["scene_category_locked"] = True
- item["scene_category_source"] = "user"
- item["scene_category"] = category
- if category not in self.CATEGORY_DESCRIPTIONS:
- item["source"] = "scene_builder_user"
- return True
- scores = self._v2_category_template(0.0)
- scores[category] = 1.0
- item["category_scores"] = dict(scores)
- item["clip_heuristic_scores"] = dict(scores)
- item["machine_primary"] = category
- item["machine_confidence"] = 1.0
- item["category_weights"] = self._v2_category_template(0.0)
- item["category_evidence_count"] = 0
- item["effective_category_scores"] = dict(scores)
- item["primary"] = category
- item["confidence"] = 1.0
- item["source"] = "scene_builder_user"
- return True
-
def _v2_auto_inject_entry_allowed(self, entry, prompt=""):
text = str(entry.get("text", "") if isinstance(entry, dict) else entry).strip()
if not text:
@@ -7718,9 +7645,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
"primary": source.get("primary", primary),
"machine_primary": source.get("machine_primary", primary),
"confidence": source.get("confidence", round(float(confidence), 4)),
- "scene_category_locked": bool(source.get("scene_category_locked")),
- "scene_category_source": source.get("scene_category_source", ""),
- "scene_category": source.get("scene_category", ""),
})
for index, phrase in enumerate(last_run.get("phrases", []) or []):
@@ -7765,7 +7689,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
add(candidate.get("text", ""), "repair_candidate", candidate, 256 + position, {})
return units[:96]
- def _v2_classify_phrases(self, clip, phrases, global_state=None, encode_cache=None, scene_db=None):
+ def _v2_classify_phrases(self, clip, phrases, global_state=None, encode_cache=None):
phrase_items = []
uncertain = []
phrase_memory = (global_state or {}).get("phrase_memory", {}) if isinstance(global_state, dict) else {}
@@ -7816,8 +7740,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
item["source"] = "clip_similarity"
for item in phrase_items:
- if self._v2_apply_scene_builder_authority(item, scene_db):
- continue
self._v2_apply_learned_category_scores(item, phrase_memory)
return phrase_items
@@ -9156,25 +9078,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
entry.setdefault("positions", {})[str(int(phrase.get("position", 0)))] = (
int(entry.setdefault("positions", {}).get(str(int(phrase.get("position", 0))), 0)) + 1
)
- if phrase.get("scene_category_locked"):
- locked_category = str(phrase.get("scene_category") or phrase.get("primary") or "details").lower()
- if locked_category in self.CATEGORY_DESCRIPTIONS:
- locked_scores = self._v2_category_template(0.0)
- locked_scores[locked_category] = 1.0
- entry["primary"] = locked_category
- entry["machine_primary"] = locked_category
- entry["category_scores"] = dict(locked_scores)
- entry["clip_heuristic_scores"] = dict(locked_scores)
- entry["effective_category_scores"] = dict(locked_scores)
- entry["confidence"] = 1.0
- entry["category_source"] = "user"
- entry["category_locked"] = True
- entry["last_seen_iter"] = int(iter_num)
- memory[text] = entry
- touched.append(text)
- if len(trained) < 8:
- trained.append(f"{text}[scene_builder:user locked]")
- continue
kind_scale = self._v2_memory_kind_scale(entry.get("kind", "phrase"))
machine_scores = self._v2_clean_category_scores(entry.get("clip_heuristic_scores", {}))
@@ -10850,70 +10753,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
parts.append(f"Pre-advisor prompt: {str(pre_advisor_prompt).strip()}")
return "\n\n".join(parts)
- def _v2_scene_builder_category_for_entry(self, entry):
- category = str(entry.get("primary", "") if isinstance(entry, dict) else "").lower()
- if category in self.CATEGORY_DESCRIPTIONS:
- return category
- scores = entry.get("effective_category_scores", entry.get("category_scores", {})) if isinstance(entry, dict) else {}
- if isinstance(scores, dict):
- primary, _ = self._v2_scores_primary(scores)
- return primary
- return self._v2_primary_category_for_text(entry.get("text", "") if isinstance(entry, dict) else entry)
-
- def _v2_scene_entry_locked(self, item):
- return isinstance(item, dict) and bool(item.get("category_locked"))
-
- def _v2_sync_scene_builder_memory(self, state, global_state, previous_run, iter_num):
- if not isinstance(state, dict) or not isinstance(previous_run, dict):
- return "Scene Builder sync: no previous run."
- scene_db = state.get("scene_builder")
- if not isinstance(scene_db, dict):
- return "Scene Builder sync: unavailable."
- memory = scene_db.setdefault("universal_memory", {})
- if not isinstance(memory, dict):
- memory = {}
- scene_db["universal_memory"] = memory
- phrase_memory = global_state.get("phrase_memory", {}) if isinstance(global_state, dict) else {}
- timestamp = self._v2_now_iso()
- touched = []
- skipped_locked = 0
- for phrase in self._v2_concept_units_for_run(previous_run):
- text = self._v2_clean_phrase_text(phrase.get("text", ""))
- if not text:
- continue
- key = self._v2_scene_key(text)
- entry = phrase_memory.get(text, phrase)
- category = self._v2_scene_builder_category_for_entry(entry)
- if category not in self.CATEGORY_DESCRIPTIONS:
- category = "details"
- existing = memory.get(key)
- if isinstance(existing, dict) and self._v2_scene_entry_locked(existing):
- existing["count"] = int(existing.get("count", 0) or 0) + 1
- existing["updated_at"] = timestamp
- skipped_locked += 1
- continue
- row = dict(existing or {})
- row["text"] = row.get("text") or text
- row["source"] = "positive"
- row["category"] = category
- row["category_source"] = "refiner"
- row["category_locked"] = False
- row["tokens"] = self._v2_phrase_words(text)
- row["count"] = int(row.get("count", 0) or 0) + 1
- row.setdefault("created_at", timestamp)
- row["updated_at"] = timestamp
- row["wildcard"] = bool(row.get("wildcard"))
- memory[key] = row
- touched.append(text)
- if len(touched) >= 24:
- break
- if not touched and not skipped_locked:
- return "Scene Builder sync: no phrase updates."
- return (
- f"Scene Builder sync: updated {len(touched)} unlocked phrase(s)"
- f"{', skipped ' + str(skipped_locked) + ' locked' if skipped_locked else ''}."
- )
-
def _v2_memory_entry_matches_current_scene(self, text, prompt, intent_phrases=None, clip=None, encode_cache=None):
clean = self._v2_clean_phrase_text(text)
prompt_clean = self._v2_clean_phrase_text(prompt)
@@ -10929,66 +10768,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
return self._v2_text_semantic_similarity(clip, clean, prompt_clean, encode_cache=encode_cache) >= 0.62
return False
- def _v2_wildcard_phrase_for_segment(self, segment, wildcard_items):
- segment_clean = self._v2_clean_phrase_text(segment)
- if not segment_clean:
- return ""
- for phrase in wildcard_items:
- if self._v2_phrase_texts_match(segment_clean, phrase):
- return phrase
- if self._v2_prompt_contains_text(segment_clean, phrase) or self._v2_prompt_contains_text(phrase, segment_clean):
- return phrase
- return ""
-
- def _v2_wildcard_phrases_similar(self, left, right, clip=None, encode_cache=None):
- if self._v2_phrase_texts_match(left, right):
- return True
- left_roots = self._v2_repair_intent_roots(left)
- right_roots = self._v2_repair_intent_roots(right)
- if left_roots and right_roots:
- overlap = left_roots & right_roots
- if overlap and (len(overlap) / float(max(1, min(len(left_roots), len(right_roots))))) >= 0.50:
- return True
- if clip is None:
- return False
- return self._v2_text_semantic_similarity(clip, left, right, encode_cache=encode_cache) >= 0.82
-
- def _v2_resolve_scene_builder_wildcards(self, prompt, scene_db, clip=None, encode_cache=None):
- if not isinstance(scene_db, dict) or not str(prompt or "").strip():
- return prompt, "Scene Builder wildcard cleanup: unavailable."
- memory = scene_db.get("universal_memory", {})
- if not isinstance(memory, dict):
- return prompt, "Scene Builder wildcard cleanup: unavailable."
- wildcard_items = [
- self._v2_clean_phrase_text(item.get("text", ""))
- for item in memory.values()
- if isinstance(item, dict) and bool(item.get("wildcard"))
- ]
- wildcard_items = [item for item in wildcard_items if item]
- if not wildcard_items:
- return prompt, "Scene Builder wildcard cleanup: none."
-
- parts = re.split(r"([,;.\n]+)", str(prompt or ""))
- kept_wildcards = []
- removed = []
- for index in range(0, len(parts), 2):
- segment = parts[index].strip()
- phrase = self._v2_wildcard_phrase_for_segment(segment, wildcard_items)
- if not phrase:
- continue
- if any(self._v2_wildcard_phrases_similar(phrase, kept, clip, encode_cache) for kept in kept_wildcards):
- removed.append(segment)
- parts[index] = ""
- continue
- kept_wildcards.append(phrase)
- if not removed:
- return prompt, "Scene Builder wildcard cleanup: no duplicates."
- 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, f"Scene Builder wildcard cleanup: removed {len(removed)} duplicate wildcard phrase(s)."
-
def _v2_compose_lucky_prompt(
self,
prompt,
@@ -11649,7 +11428,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
extra_reset = self._v2_reset_prompt_keys(refinement_key, _raw_positive_prompt, _raw_intent_prompt)
if extra_reset:
state_status = f"{state_status} (also reset keys: {', '.join(extra_reset)})"
- scene_db, scene_builder_status = self._v2_scene_builder_db(state)
global_state = state.setdefault("global", {})
global_state.setdefault("phrase_memory", {})
global_state.setdefault("axis_conditioning_memory", {})
@@ -11718,22 +11496,14 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
int(global_state.get("total_iterations", 0)) + 1,
clip=clip,
encode_cache=encode_cache,
- scene_db=scene_db,
seed_output_connected=bool(seed_output_connected),
refinement_key=refinement_key,
)
memory_status = me_status
seed_memory_status = ""
- scene_sync_status = self._v2_sync_scene_builder_memory(
- state,
- global_state,
- previous_run,
- int(global_state.get("total_iterations", 0)) + 1,
- )
intent_family_status = ""
intent_learning_status = ""
negative_memory_status = ""
- memory_status = f"{memory_status}\n{scene_sync_status}"
if aggregate_profile and has_previous_run and not aggregate_profile.get("skip_learning"):
self._v2_update_streaks(global_state, aggregate_profile, update_conditioning_strength=not prompt_only_mode)
repair_feedback, repair_persistence_status = self._v2_active_repair_feedback(
@@ -11760,12 +11530,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
int(global_state.get("total_iterations", 0)) + 1,
seed_output_connected=bool(seed_output_connected),
)
- scene_sync_status = self._v2_sync_scene_builder_memory(
- state,
- global_state,
- previous_run,
- int(global_state.get("total_iterations", 0)) + 1,
- )
intent_family_status, _ = self._v2_update_intent_family_memory(
global_state,
previous_run,
@@ -11792,7 +11556,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
int(global_state.get("total_iterations", 0)) + 1,
axis_feedback,
)
- memory_status = f"{memory_status}\n{seed_memory_status}\n{scene_sync_status}\n{intent_family_status}\n{intent_learning_status}"
+ memory_status = f"{memory_status}\n{seed_memory_status}\n{intent_family_status}\n{intent_learning_status}"
self._v2_update_conditioning_memory(global_state, previous_run, learning_profile, axis_feedback)
# Always train the value function in the background (cheap: a small MLP, no diffusion
# calls) so the reward asset accumulates regardless of whether guidance is applied.
@@ -11846,7 +11610,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
self._ordered_prompt_phrases(analysis_prompt),
global_state,
encode_cache=encode_cache,
- scene_db=scene_db,
)
intent_phrases = []
if intent_prompt and not intent_prompt_is_vague and not current_prompt_refusal:
@@ -11855,7 +11618,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
self._ordered_prompt_phrases(intent_prompt),
global_state,
encode_cache=encode_cache,
- scene_db=scene_db,
)
intent_source_prompt = self._v2_intent_source_prompt(analysis_prompt, intent_prompt, intent_prompt_is_vague)
current_family_key, current_family_slot, current_family_similarity = self._v2_intent_family_slot(
@@ -11879,7 +11641,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
intent_alignment_adjustments = []
perfect_repair_status = "Perfect repairs: none."
perfect_repair_adjustments = []
- wildcard_status = "Scene Builder wildcard cleanup: none."
advisor_status = "Advisor: off."
model_patch_status = "Model patch: no model connected."
advisor_diagnostic = ""
@@ -11907,11 +11668,10 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
refusal_status = "Current prompt refused by enhancer; storage skipped."
elif learning_mode:
prompt_to_encode = analysis_prompt
- lucky_status = "Learning mode: observing only; Lucky, intent alignment, prompt repair, and wildcard cleanup skipped."
+ lucky_status = "Learning mode: observing only; Lucky, intent alignment, and prompt repair skipped."
intent_alignment_status = "Intent alignment: skipped in Learning mode."
perfect_repair_status = "Perfect repairs: skipped in Learning mode."
repair_status = "Prompt repair: skipped in Learning mode."
- wildcard_status = "Scene Builder wildcard cleanup: skipped in Learning mode."
encoded_role = "learning passthrough"
elif perfect_freeze:
frozen = self._v2_prompt_key(previous_run.get("encoded_prompt", "")) or analysis_prompt
@@ -11920,7 +11680,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
intent_alignment_status = "Intent alignment: skipped (perfect freeze)."
perfect_repair_status = "Perfect repairs: skipped (perfect freeze)."
repair_status = "Prompt repair: skipped (perfect freeze)."
- wildcard_status = "Scene Builder wildcard cleanup: skipped (perfect freeze)."
encoded_role = "perfect frozen prompt"
elif im_feeling_lucky and clip is not None:
prompt_to_encode, lucky_status = self._v2_compose_lucky_prompt(
@@ -11932,12 +11691,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
clip=clip,
encode_cache=encode_cache,
)
- prompt_to_encode, wildcard_status = self._v2_resolve_scene_builder_wildcards(
- prompt_to_encode,
- scene_db,
- clip=clip,
- encode_cache=encode_cache,
- )
encoded_role = "lucky prompt"
else:
aligned_prompt, intent_alignment_status, intent_alignment_adjustments = self._v2_apply_intent_alignment_memory(
@@ -11957,12 +11710,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
emphasis_status = "Prompt emphasis: removed."
advisor_active = advisor_mode != "Off"
repair_status = "Prompt repair: removed."
- prompt_to_encode, wildcard_status = self._v2_resolve_scene_builder_wildcards(
- prompt_to_encode,
- scene_db,
- clip=clip,
- encode_cache=encode_cache,
- )
if im_feeling_lucky and clip is None:
lucky_status = f"Lucky: unavailable without CLIP | connected CONDITIONING accepted. {emphasis_status}"
else:
@@ -12310,7 +12057,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
"Memory\n"
f"{self._v2_axis_feedback_status(axis_feedback)}\n"
f"{memory_status}"
- + (f"\n{_active(scene_builder_status)}" if _active(scene_builder_status) else "")
+ (f"\n{_active(repair_persistence_status)}" if _active(repair_persistence_status) else "")
),
(
@@ -12330,7 +12076,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
_active(shortcut_status),
_active(advisor_status),
_active(lucky_status),
- _active(wildcard_status),
_active(vision_status),
model_patch_status if model is not None else "",
refusal_status if refusal_status else "",
@@ -12341,7 +12086,6 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
_active(shortcut_status),
_active(advisor_status),
_active(lucky_status),
- _active(wildcard_status),
_active(vision_status),
model_patch_status if model is not None else "",
refusal_status if refusal_status else "",
@@ -14102,13 +13846,7 @@ class FunPackStudio:
except Exception:
settings = {}
- # --- Scene Builder ---
- sb = settings.get("scene_builder", {}) if isinstance(settings.get("scene_builder"), dict) else {}
- sb_mode = str(sb.get("mode", "Pass-through")).strip()
- if sb_mode == "Pass-through" or not sb_mode:
- active_prompt = str(positive_prompt or "").strip()
- else:
- active_prompt = str(sb.get("scene_positive", "") or "").strip() or str(positive_prompt or "").strip()
+ active_prompt = str(positive_prompt or "").strip()
# --- Negative conditioning ---
# Priority: pre-encoded input > encode negative_prompt via CLIP > popup default > empty
diff --git a/docs/FunPackSceneBuilder.md b/docs/FunPackSceneBuilder.md
deleted file mode 100644
index eb8287b0..00000000
--- a/docs/FunPackSceneBuilder.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# 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 and useful words into universal memory, but the node outputs composed scene text or pass-through text in Learning mode.
-
-## Visible Controls
-
-The node surface is a compact launcher. Scene name, mode, aliases, prompt editors, saved scenes, and database controls live in the centered custom editor UI.
-
-If no scene exists yet, opening the editor, prompt editor, or database first asks for a new scene name.
-
-## Editor Menus
-
-**Positive prompt**: Opens a combined text and phrase-button composer. Typed text, commas, periods, and manual ordering are preserved exactly. Database chips can be clicked or dragged into the text field, filtered by category, and chips already present in the prompt appear as removable buttons inside the same prompt field.
-
-**Preconstruct**: In the positive prompt editor, this button adds matching database phrases for the current intent into the editor text. It only runs when you press the button; queueing the node never preconstructs automatically.
-
-**Negative prompt**: Same composer behavior for the negative prompt.
-
-**Database**: Add, edit, delete, search, categorize, and mark universal words and phrases as wildcards. Double-click a word or phrase to edit it inline, then use **OK** or **Cancel**.
-
-**Shortcuts**: Add global activation phrases that expand into longer positive prompt text. Shortcuts are shared across refinement keys, can be imported/exported separately, and are not cleared by Refiner reset. Activation phrases replace only the matched word or phrase and preserve surrounding spaces and punctuation.
-
-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.
-
-Each editor menu has **Back**, **Cancel**, and **Confirm** controls. Back always returns to the main editor. Cancel restores the menu snapshot; if the prompt/database editor was opened directly from the node, Cancel closes the dialog instead.
-
-## Modes
-
-**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 and useful words into Scene Builder universal memory, then passes the connected positive prompt, negative prompt, and LoRA stack through unchanged.
-
-Shortcuts apply to positive prompt output in all modes, including Learning pass-through. Negative prompts are never expanded.
-
-## Connection-Only Inputs
-
-**intent_prompt**: Text used for Auto scene detection. This can be the same intent prompt that feeds Refiner V2.
-
-**positive_prompt** and **negative_prompt**: Universal memory sources. They do not override scene output except in Learning pass-through mode.
-
-**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.
-
-Scene Builder memory is stored in the selected refinement key under its own `scene_builder` section. Refiner reset clears conditioning-delta learning and prompt histories, but preserves Scene Builder universal memory and saved scenes.
-
-## Outputs
-
-**positive_prompt**: Composed positive scene text.
-
-**negative_prompt**: Composed negative scene text.
-
-**lora_stack**: The connected `FUNPACK_LORA_STACK`, passed through unchanged.
-
-**status**: Summary of selected mode, matched scene, phrase counts, and pass-through LoRA stack count.
-
-## Wildcards
-
-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 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.
-6. Use **Learning** when generating changing prompts and you want Scene Builder to collect memory while leaving the generation prompt untouched.
diff --git a/docs/FunPackStudio.md b/docs/FunPackStudio.md
index 2028e98f..9ffdbbde 100644
--- a/docs/FunPackStudio.md
+++ b/docs/FunPackStudio.md
@@ -1,6 +1,6 @@
# FunPack Studio
-`FunPack Studio` is a single node that combines all core FunPack refinement tools under one interface. It replaces the typical chain of `FunPack Refinement Key Loader - FunPack Scene Builder - FunPack Apply LoRA Weights - FunPack LoRA Loader - FunPack Video Refiner V2 - FunPack Conditioning Adjust` with a single node and a tabbed popup editor.
+`FunPack Studio` is a single node that combines all core FunPack refinement tools under one interface. It replaces the typical chain of `FunPack Refinement Key Loader - FunPack Apply LoRA Weights - FunPack LoRA Loader - FunPack Video Refiner V2 - FunPack Conditioning Adjust` with a single node and a tabbed popup editor.
The standalone nodes remain fully functional. Studio is an alternative for workflows where you want everything in one place.
@@ -22,7 +22,7 @@ The `rating` widget opens a **rating picker** popup. Alongside the quality ratin
| `clip_vision_output` | CLIP_VISION_OUTPUT | Advisory image context stored in session state. Not blended into conditioning. |
| `source_image` | IMAGE | Source image or frame batch. V2 stores size, aspect ratio, and a fingerprint to detect changes. |
| `lora_stack` | FUNPACK_LORA_STACK | External LoRA stack. Bypasses Studio's internal LoRA management entirely when connected. |
-| `positive_prompt` | STRING | Positive prompt text. Ignored when Scene Builder mode is not Pass-through. |
+| `positive_prompt` | STRING | Positive prompt text. |
| `negative_prompt` | STRING | Negative prompt text. Encoded via CLIP and output as negative conditioning. Skipped when `negative_conditioning` is connected. |
| `user_intent_prompt` | STRING | Raw user intent for repair and alignment anchoring. |
| `feedback_prompt` | STRING | Feedback describing what was wrong with the previous output. Highest priority in the advisor. |
@@ -53,17 +53,9 @@ Set the refinement key for the current session. This key links all FunPack memor
When a `refinement_key_input` is connected to the node, that value takes precedence at runtime unless the **Override** toggle is enabled.
-**Scene Builder mode** is also controlled here. Changing from Pass-through to Manual/Auto/Learning activates the scene builder, which then constructs the positive prompt from the Scene tab instead of from the `positive_prompt` input.
-
-### Scene
-
-Available when Scene Builder mode is not Pass-through. Save and load named scene presets. The phrase bank shows all phrases from the session's universal memory. Clicking a chip appends it to the positive prompt composer.
-
-When Scene Builder is active the Refiner tab shows a notice and the intent override field is disabled, since intent derives from the scene prompt.
-
### Shortcuts
-Shortcuts are global positive-prompt expansions shared by Scene Builder, Studio, and Refiner V2. Each shortcut has one or more activation phrases and one or more replacement phrases. Matching uses exact word or phrase boundaries, preserves surrounding spaces and punctuation, and chooses among replacements with the run seed.
+Shortcuts are global positive-prompt expansions shared by Studio and Refiner V2. Each shortcut has one or more activation phrases and one or more replacement phrases. Matching uses exact word or phrase boundaries, preserves surrounding spaces and punctuation, and chooses among replacements with the run seed.
Shortcuts are stored outside refinement keys, can be imported/exported separately, and are not cleared by **Reset session**.
diff --git a/example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json b/example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json
deleted file mode 100644
index 8898907c..00000000
--- a/example_workflows/FunPack_LTX23_SceneBuilder_Advisor_Updated.json
+++ /dev/null
@@ -1,5411 +0,0 @@
-{
- "id": "f725183c-5c05-4371-93c4-b4c8a25ea8ca",
- "revision": 0,
- "last_node_id": 5284,
- "last_link_id": 14723,
- "nodes": [
- {
- "id": 5167,
- "type": "KSamplerSelect",
- "pos": [
- -2699.415744610746,
- 4033.142314086365
- ],
- "size": [
- 270,
- 58
- ],
- "flags": {},
- "order": 0,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "SAMPLER",
- "type": "SAMPLER",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "KSamplerSelect"
- },
- "widgets_values": [
- "euler"
- ]
- },
- {
- "id": 5168,
- "type": "PreviewAny",
- "pos": [
- -4385.226512535651,
- 3084.645124512045
- ],
- "size": [
- 289.4653455216139,
- 343.798203293964
- ],
- "flags": {},
- "order": 25,
- "mode": 0,
- "inputs": [
- {
- "name": "source",
- "type": "*",
- "link": 14514
- }
- ],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": null
- }
- ],
- "title": "LoRA Loader Status",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.19.5",
- "Node name for S&R": "PreviewAny"
- },
- "widgets_values": [
- null,
- null,
- null
- ],
- "color": "#223",
- "bgcolor": "#335"
- },
- {
- "id": 4845,
- "type": "LTXVSeparateAVLatent",
- "pos": [
- -2401.917798285339,
- 3489.1800825826076
- ],
- "size": [
- 271.2833312988281,
- 46
- ],
- "flags": {},
- "order": 34,
- "mode": 0,
- "inputs": [
- {
- "name": "av_latent",
- "type": "LATENT",
- "link": 13890
- }
- ],
- "outputs": [
- {
- "name": "video_latent",
- "type": "LATENT",
- "links": [
- 14298,
- 14480
- ]
- },
- {
- "name": "audio_latent",
- "type": "LATENT",
- "links": [
- 13455,
- 14302
- ]
- }
- ],
- "title": "Separate Low Resolution AV Latent",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.10.0",
- "Node name for S&R": "LTXVSeparateAVLatent"
- },
- "widgets_values": [],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 5198,
- "type": "LTXVSeparateAVLatent",
- "pos": [
- -1541.2540117017459,
- 3488.6906320381736
- ],
- "size": [
- 271.2833312988281,
- 46
- ],
- "flags": {},
- "order": 42,
- "mode": 0,
- "inputs": [
- {
- "name": "av_latent",
- "type": "LATENT",
- "link": 14303
- }
- ],
- "outputs": [
- {
- "name": "video_latent",
- "type": "LATENT",
- "links": [
- 14304
- ]
- },
- {
- "name": "audio_latent",
- "type": "LATENT",
- "links": [
- 14306
- ]
- }
- ],
- "title": "Separate Low Resolution AV Latent",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.10.0",
- "Node name for S&R": "LTXVSeparateAVLatent"
- },
- "widgets_values": [],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 5147,
- "type": "LTXVConditioning",
- "pos": [
- -3009.7900745646834,
- 3598.4938749825565
- ],
- "size": [
- 296.9524603795917,
- 79.1230191824834
- ],
- "flags": {},
- "order": 29,
- "mode": 0,
- "inputs": [
- {
- "name": "positive",
- "type": "CONDITIONING",
- "link": 14685
- },
- {
- "name": "negative",
- "type": "CONDITIONING",
- "link": 14672
- },
- {
- "name": "frame_rate",
- "type": "FLOAT",
- "widget": {
- "name": "frame_rate"
- },
- "link": 14088
- }
- ],
- "outputs": [
- {
- "name": "positive",
- "type": "CONDITIONING",
- "links": [
- 14278,
- 14317
- ]
- },
- {
- "name": "negative",
- "type": "CONDITIONING",
- "links": [
- 14279,
- 14318
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.19.5",
- "Node name for S&R": "LTXVConditioning"
- },
- "widgets_values": [
- 25
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5200,
- "type": "LTXVAudioVAEDecode",
- "pos": [
- -1539.6670726341526,
- 3822.926110841993
- ],
- "size": [
- 269.4217902135765,
- 47.123019182483404
- ],
- "flags": {},
- "order": 44,
- "mode": 0,
- "inputs": [
- {
- "name": "samples",
- "type": "LATENT",
- "link": 14306
- },
- {
- "name": "audio_vae",
- "type": "VAE",
- "link": 14308
- }
- ],
- "outputs": [
- {
- "name": "Audio",
- "type": "AUDIO",
- "links": [
- 14450
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.3.64",
- "Node name for S&R": "LTXVAudioVAEDecode"
- },
- "widgets_values": [],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 5234,
- "type": "NormalizeAudioLoudness",
- "pos": [
- -1536.735391987636,
- 3911.7362407151004
- ],
- "size": [
- 277.5000061035156,
- 58
- ],
- "flags": {},
- "order": 45,
- "mode": 0,
- "inputs": [
- {
- "name": "audio",
- "type": "AUDIO",
- "link": 14450
- }
- ],
- "outputs": [
- {
- "name": "audio",
- "type": "AUDIO",
- "links": [
- 14451
- ]
- }
- ],
- "properties": {
- "aux_id": "Kijai/ComfyUI-WanVideoWrapper",
- "ver": "d18cdb18597f525ef8d613a0cb447080fbab8fce",
- "Node name for S&R": "NormalizeAudioLoudness"
- },
- "widgets_values": [
- -22
- ]
- },
- {
- "id": 5021,
- "type": "LTXVAudioVAEDecode",
- "pos": [
- -2394.239875549051,
- 3818.9126090617733
- ],
- "size": [
- 269.4217902135765,
- 47.123019182483404
- ],
- "flags": {},
- "order": 37,
- "mode": 0,
- "inputs": [
- {
- "name": "samples",
- "type": "LATENT",
- "link": 13455
- },
- {
- "name": "audio_vae",
- "type": "VAE",
- "link": 14051
- }
- ],
- "outputs": [
- {
- "name": "Audio",
- "type": "AUDIO",
- "links": [
- 14285
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.3.64",
- "Node name for S&R": "LTXVAudioVAEDecode"
- },
- "widgets_values": [],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 4528,
- "type": "LTXVConcatAVLatent",
- "pos": [
- -3011.830110250156,
- 3717.5362022165496
- ],
- "size": [
- 295.20239189803476,
- 47.12301918248295
- ],
- "flags": {},
- "order": 27,
- "mode": 0,
- "inputs": [
- {
- "name": "video_latent",
- "type": "LATENT",
- "link": 14600
- },
- {
- "name": "audio_latent",
- "type": "LATENT",
- "link": 14064
- }
- ],
- "outputs": [
- {
- "name": "latent",
- "type": "LATENT",
- "links": [
- 13963
- ]
- }
- ],
- "title": "Concat Refined Video + Audio Latents",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.10.0",
- "Node name for S&R": "LTXVConcatAVLatent"
- },
- "widgets_values": [],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 5192,
- "type": "LTXVLatentUpsampler",
- "pos": [
- -2396.1920366029976,
- 3909.1698554067884
- ],
- "size": [
- 275.48004914142155,
- 71.78799059939456
- ],
- "flags": {},
- "order": 35,
- "mode": 0,
- "inputs": [
- {
- "name": "samples",
- "type": "LATENT",
- "link": 14298
- },
- {
- "name": "upscale_model",
- "type": "LATENT_UPSCALE_MODEL",
- "link": 14301
- },
- {
- "name": "vae",
- "type": "VAE",
- "link": 14300
- }
- ],
- "outputs": [
- {
- "name": "LATENT",
- "type": "LATENT",
- "links": [
- 14323
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "LTXVLatentUpsampler"
- },
- "widgets_values": [],
- "color": "#232",
- "bgcolor": "#353"
- },
- {
- "id": 5196,
- "type": "LatentUpscaleModelLoader",
- "pos": [
- -2705.3565078421434,
- 4292.439225769344
- ],
- "size": [
- 285.1080109057697,
- 63.260013632212576
- ],
- "flags": {},
- "order": 1,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "LATENT_UPSCALE_MODEL",
- "type": "LATENT_UPSCALE_MODEL",
- "links": [
- 14301
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "LatentUpscaleModelLoader"
- },
- "widgets_values": [
- "ltx2_spatial_upscaler.safetensors"
- ],
- "color": "#223",
- "bgcolor": "#335"
- },
- {
- "id": 5150,
- "type": "MarkdownNote",
- "pos": [
- -4874.5803841744955,
- 4030.3082654487844
- ],
- "size": [
- 457.2957979656867,
- 322.28010914237484
- ],
- "flags": {},
- "order": 2,
- "mode": 0,
- "inputs": [],
- "outputs": [],
- "properties": {},
- "widgets_values": [
- "This is an example workflow, finetuned for LTX2.3 video model, using FunPack nodes to enhance and correct the results, tweaking them according to user's preferences.\n\nVideo Refiner V2 node learns from your ratings and feedback to provide the best result much more consistently than a base model does.\n\nThis workflow DOES NOT make model learn concepts it were never aware of. It only finetunes conditioning to match what YOU like as user, however it can't introduce knowledge that was never a part of a model.\n\nThis example workflow is actively using subgraphs to compact the space it takes, providing much more neat and clear workspace.\n\nIf you have any feedback or you found a bug or you have problems with using my nodes, feel free to file an issue on Github:\n\nhttps://github.com/digital-garbage/ComfyUI-FunPack\n\nPlease make sure you are reporting issues while using \"main\" branch of the repo. Issues with \"dev\" branch will not be reviewed unless they are critical bugs, breaking workflow fully."
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5195,
- "type": "LTXVConcatAVLatent",
- "pos": [
- -2406.69882103561,
- 4022.014876163903
- ],
- "size": [
- 291.79999389648435,
- 47.84435512229766
- ],
- "flags": {},
- "order": 40,
- "mode": 0,
- "inputs": [
- {
- "name": "video_latent",
- "type": "LATENT",
- "link": 14324
- },
- {
- "name": "audio_latent",
- "type": "LATENT",
- "link": 14302
- }
- ],
- "outputs": [
- {
- "name": "latent",
- "type": "LATENT",
- "links": [
- 14297
- ]
- }
- ],
- "title": "Concat Refined Video + Audio Latents",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.10.0",
- "Node name for S&R": "LTXVConcatAVLatent"
- },
- "widgets_values": [],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 5199,
- "type": "LTXVTiledVAEDecode",
- "pos": [
- -1541.4513358307363,
- 3578.1428461746864
- ],
- "size": [
- 269.8901736365534,
- 198
- ],
- "flags": {},
- "order": 43,
- "mode": 0,
- "inputs": [
- {
- "name": "vae",
- "type": "VAE",
- "link": 14309
- },
- {
- "name": "latents",
- "type": "LATENT",
- "link": 14304
- }
- ],
- "outputs": [
- {
- "name": "image",
- "type": "IMAGE",
- "links": [
- 14305
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-LTXVideo",
- "ver": "d43181d5723c1721692baef01d90cd1a825bba91",
- "Node name for S&R": "LTXVTiledVAEDecode",
- "aux_id": "Lightricks/ComfyUI-LTXVideo"
- },
- "widgets_values": [
- 2,
- 1,
- 6,
- true,
- "auto",
- "auto"
- ],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 5205,
- "type": "LTXVImgToVideoInplace",
- "pos": [
- -1835.3376768131275,
- 4171.647861437848
- ],
- "size": [
- 280.36925823128377,
- 127.59106100617828
- ],
- "flags": {},
- "order": 38,
- "mode": 0,
- "inputs": [
- {
- "name": "vae",
- "type": "VAE",
- "link": 14325
- },
- {
- "name": "image",
- "type": "IMAGE",
- "link": 14326
- },
- {
- "name": "latent",
- "type": "LATENT",
- "link": 14323
- }
- ],
- "outputs": [
- {
- "name": "latent",
- "type": "LATENT",
- "links": [
- 14324
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.7.0",
- "Node name for S&R": "LTXVImgToVideoInplace",
- "ue_properties": {
- "widget_ue_connectable": {},
- "version": "7.5.2",
- "input_ue_unconnectable": {}
- }
- },
- "widgets_values": [
- 1,
- false
- ],
- "color": "#232",
- "bgcolor": "#353"
- },
- {
- "id": 5197,
- "type": "VHS_VideoCombine",
- "pos": [
- -1245.5962746472371,
- 3490.3936722085596
- ],
- "size": [
- 250,
- 310
- ],
- "flags": {},
- "order": 46,
- "mode": 0,
- "inputs": [
- {
- "name": "images",
- "type": "IMAGE",
- "link": 14305
- },
- {
- "name": "audio",
- "shape": 7,
- "type": "AUDIO",
- "link": 14451
- },
- {
- "name": "meta_batch",
- "shape": 7,
- "type": "VHS_BatchManager",
- "link": null
- },
- {
- "name": "vae",
- "shape": 7,
- "type": "VAE",
- "link": null
- },
- {
- "name": "frame_rate",
- "type": "FLOAT",
- "widget": {
- "name": "frame_rate"
- },
- "link": 14331
- }
- ],
- "outputs": [
- {
- "name": "Filenames",
- "type": "VHS_FILENAMES",
- "links": null
- }
- ],
- "title": "Result",
- "properties": {
- "cnr_id": "comfyui-videohelpersuite",
- "ver": "2984ec4c4b93292421888f38db74a5e8802a8ff8",
- "Node name for S&R": "VHS_VideoCombine",
- "aux_id": "Kosinkadink/ComfyUI-VideoHelperSuite"
- },
- "widgets_values": {
- "frame_rate": 8,
- "loop_count": 0,
- "filename_prefix": "result_%date:MM-dd_hh-mm%",
- "format": "video/h265-mp4",
- "pix_fmt": "yuv420p",
- "crf": 20,
- "save_metadata": true,
- "pingpong": false,
- "save_output": false,
- "videopreview": {
- "hidden": false,
- "paused": false,
- "params": {
- "filename": "result_05-14_18-03_00001-audio.mp4",
- "subfolder": "",
- "type": "temp",
- "format": "video/h265-mp4",
- "frame_rate": 30,
- "workflow": "result_05-14_18-03_00001.png",
- "fullpath": "/workspace/ComfyUI/temp/result_05-14_18-03_00001-audio.mp4"
- }
- }
- },
- "color": "#222",
- "bgcolor": "#000"
- },
- {
- "id": 5193,
- "type": "SamplerCustom",
- "pos": [
- -1834.8950107950545,
- 3485.2764730367944
- ],
- "size": [
- 280,
- 644.9638554216867
- ],
- "flags": {},
- "order": 41,
- "mode": 0,
- "inputs": [
- {
- "name": "model",
- "type": "MODEL",
- "link": 14695
- },
- {
- "name": "positive",
- "type": "CONDITIONING",
- "link": 14317
- },
- {
- "name": "negative",
- "type": "CONDITIONING",
- "link": 14318
- },
- {
- "name": "sampler",
- "type": "SAMPLER",
- "link": 14721
- },
- {
- "name": "sigmas",
- "type": "SIGMAS",
- "link": 14722
- },
- {
- "name": "latent_image",
- "type": "LATENT",
- "link": 14297
- }
- ],
- "outputs": [
- {
- "name": "output",
- "type": "LATENT",
- "links": [
- 14303
- ]
- },
- {
- "name": "denoised_output",
- "type": "LATENT",
- "links": null
- }
- ],
- "title": "Sampler",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.19.3",
- "Node name for S&R": "SamplerCustom"
- },
- "widgets_values": [
- true,
- 592208988108730,
- "randomize",
- 1
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 5020,
- "type": "LTXVTiledVAEDecode",
- "pos": [
- -2401.203278845233,
- 3577.45748585416
- ],
- "size": [
- 269.8901736365534,
- 198
- ],
- "flags": {},
- "order": 36,
- "mode": 0,
- "inputs": [
- {
- "name": "vae",
- "type": "VAE",
- "link": 14055
- },
- {
- "name": "latents",
- "type": "LATENT",
- "link": 14480
- }
- ],
- "outputs": [
- {
- "name": "image",
- "type": "IMAGE",
- "links": [
- 14011
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-LTXVideo",
- "ver": "d43181d5723c1721692baef01d90cd1a825bba91",
- "Node name for S&R": "LTXVTiledVAEDecode",
- "aux_id": "Lightricks/ComfyUI-LTXVideo"
- },
- "widgets_values": [
- 2,
- 2,
- 6,
- true,
- "auto",
- "auto"
- ],
- "color": "#323",
- "bgcolor": "#535"
- },
- {
- "id": 5145,
- "type": "LTXFloatToInt",
- "pos": [
- -3717.4436300427296,
- 4331.859884921647
- ],
- "size": [
- 270,
- 58
- ],
- "flags": {
- "collapsed": true
- },
- "order": 17,
- "mode": 0,
- "inputs": [
- {
- "name": "a",
- "type": "FLOAT",
- "widget": {
- "name": "a"
- },
- "link": 14066
- }
- ],
- "outputs": [
- {
- "name": "INT",
- "type": "INT",
- "links": [
- 14067
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-LTXVideo",
- "ver": "937831df0e5e1f707340b7e037a52e9d1196e3f8",
- "Node name for S&R": "LTXFloatToInt",
- "aux_id": "Lightricks/ComfyUI-LTXVideo"
- },
- "widgets_values": [
- 0
- ]
- },
- {
- "id": 5153,
- "type": "PreviewAny",
- "pos": [
- -2402.193396803096,
- 3097.278374509551
- ],
- "size": [
- 839.175670995261,
- 342.8519370213312
- ],
- "flags": {},
- "order": 31,
- "mode": 0,
- "inputs": [
- {
- "name": "source",
- "type": "*",
- "link": 14687
- }
- ],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": null
- }
- ],
- "title": "Training Info",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.19.5",
- "Node name for S&R": "PreviewAny"
- },
- "widgets_values": [
- null,
- null,
- null
- ]
- },
- {
- "id": 5101,
- "type": "SamplerCustom",
- "pos": [
- -2700.2357314296046,
- 3490.168714859668
- ],
- "size": [
- 280,
- 644.9638554216867
- ],
- "flags": {},
- "order": 33,
- "mode": 0,
- "inputs": [
- {
- "name": "model",
- "type": "MODEL",
- "link": 14694
- },
- {
- "name": "positive",
- "type": "CONDITIONING",
- "link": 14278
- },
- {
- "name": "negative",
- "type": "CONDITIONING",
- "link": 14279
- },
- {
- "name": "sampler",
- "type": "SAMPLER",
- "link": 14691
- },
- {
- "name": "sigmas",
- "type": "SIGMAS",
- "link": 14692
- },
- {
- "name": "latent_image",
- "type": "LATENT",
- "link": 13963
- }
- ],
- "outputs": [
- {
- "name": "output",
- "type": "LATENT",
- "links": [
- 13890
- ]
- },
- {
- "name": "denoised_output",
- "type": "LATENT",
- "links": null
- }
- ],
- "title": "Sampler",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.19.3",
- "Node name for S&R": "SamplerCustom"
- },
- "widgets_values": [
- true,
- 715786071341859,
- "randomize",
- 1
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 5202,
- "type": "ManualSigmas",
- "pos": [
- -2401.622706019997,
- 4283.142065776868
- ],
- "size": [
- 286.5405142205909,
- 70.24154208314985
- ],
- "flags": {},
- "order": 3,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "SIGMAS",
- "type": "SIGMAS",
- "links": [
- 14658
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.14.1",
- "Node name for S&R": "ManualSigmas"
- },
- "widgets_values": [
- "0.909375, 0.725, 0.421875, 0.0"
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 5170,
- "type": "KSamplerSelect",
- "pos": [
- -2707.722884971137,
- 4189.687913309725
- ],
- "size": [
- 286.83204362307833,
- 63.61068120769323
- ],
- "flags": {},
- "order": 4,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "SAMPLER",
- "type": "SAMPLER",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "KSamplerSelect"
- },
- "widgets_values": [
- "euler_cfg_pp"
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 5050,
- "type": "ManualSigmas",
- "pos": [
- -3008.3676712676593,
- 3487.1474804993636
- ],
- "size": [
- 297.7618766359765,
- 71.64421238507339
- ],
- "flags": {},
- "order": 5,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "SIGMAS",
- "type": "SIGMAS",
- "links": [
- 14655,
- 14666
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.14.1",
- "Node name for S&R": "ManualSigmas"
- },
- "widgets_values": [
- "1.0, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0"
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 5177,
- "type": "PreviewAny",
- "pos": [
- -3504.032993175745,
- 3095.0195440692464
- ],
- "size": [
- 457.7306854704266,
- 356.91212124433196
- ],
- "flags": {},
- "order": 19,
- "mode": 0,
- "inputs": [
- {
- "name": "source",
- "type": "*",
- "link": 14282
- }
- ],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": []
- }
- ],
- "title": "Enhanced Prompt Preview",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "PreviewAny"
- },
- "widgets_values": [
- null,
- null,
- null
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5144,
- "type": "2d968b7e-61e9-49f7-a0a5-bb17386fa820",
- "pos": [
- -4051.876461790305,
- 3831.343930764919
- ],
- "size": [
- 508.9639946472771,
- 210
- ],
- "flags": {},
- "order": 23,
- "mode": 0,
- "inputs": [
- {
- "name": "length",
- "type": "INT",
- "widget": {
- "name": "length"
- },
- "link": 14059
- },
- {
- "name": "vae",
- "type": "VAE",
- "link": 14060
- },
- {
- "name": "audio_vae",
- "type": "VAE",
- "link": 14061
- },
- {
- "name": "image",
- "type": "IMAGE,MASK",
- "link": 14062
- },
- {
- "name": "frame_rate",
- "type": "INT",
- "widget": {
- "name": "frame_rate"
- },
- "link": 14067
- }
- ],
- "outputs": [
- {
- "label": "video_latent",
- "name": "latent",
- "type": "LATENT",
- "links": [
- 14600
- ]
- },
- {
- "label": "audio_latent",
- "name": "Latent",
- "type": "LATENT",
- "links": [
- 14064
- ]
- },
- {
- "name": "output_image",
- "type": "IMAGE",
- "links": [
- 14326,
- 14682
- ]
- },
- {
- "name": "LATENT",
- "type": "LATENT",
- "links": []
- },
- {
- "label": "plain_black",
- "name": "output",
- "type": "IMAGE",
- "links": []
- }
- ],
- "properties": {
- "proxyWidgets": [
- [
- "3059",
- "length"
- ],
- [
- "3980",
- "frame_rate"
- ],
- [
- "5031",
- "target_width"
- ],
- [
- "5031",
- "target_height"
- ]
- ],
- "cnr_id": "comfy-core",
- "ver": "0.19.5"
- },
- "widgets_values": [],
- "color": "#232",
- "bgcolor": "#353"
- },
- {
- "id": 5232,
- "type": "PreviewAny",
- "pos": [
- -3008.8456341301176,
- 3094.305546134133
- ],
- "size": [
- 584.4302826165278,
- 339.8875564758546
- ],
- "flags": {},
- "order": 30,
- "mode": 0,
- "inputs": [
- {
- "name": "source",
- "type": "*",
- "link": 14686
- }
- ],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": null
- }
- ],
- "title": "Status",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.19.5",
- "Node name for S&R": "PreviewAny"
- },
- "widgets_values": [
- null,
- null,
- null
- ]
- },
- {
- "id": 5229,
- "type": "LTX2_NAG",
- "pos": [
- -4049.362811312737,
- 3096.7926698211413
- ],
- "size": [
- 502.1137448018944,
- 171.12950727397492
- ],
- "flags": {},
- "order": 24,
- "mode": 0,
- "inputs": [
- {
- "name": "model",
- "type": "MODEL",
- "link": 14426
- },
- {
- "name": "nag_cond_video",
- "shape": 7,
- "type": "CONDITIONING",
- "link": null
- },
- {
- "name": "nag_cond_audio",
- "shape": 7,
- "type": "CONDITIONING",
- "link": null
- }
- ],
- "outputs": [
- {
- "name": "model",
- "type": "MODEL",
- "links": [
- 14699
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "9c3255f39cd2ea0ebf94a2016af4bd2e5d6eb91b",
- "Node name for S&R": "LTX2_NAG",
- "aux_id": "kijai/ComfyUI-KJNodes"
- },
- "widgets_values": [
- 11,
- 0.3,
- 0.275,
- true
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5272,
- "type": "FunPackHybridEuler2SSampler",
- "pos": [
- -3022.600582098002,
- 3970.934589994262
- ],
- "size": [
- 298.99769408074997,
- 393.0067483633211
- ],
- "flags": {},
- "order": 16,
- "mode": 0,
- "inputs": [
- {
- "name": "sigmas",
- "shape": 7,
- "type": "SIGMAS",
- "link": 14666
- }
- ],
- "outputs": [
- {
- "name": "sampler",
- "type": "SAMPLER",
- "links": []
- },
- {
- "name": "sigmas",
- "type": "SIGMAS",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "567f0a66ed7f0683204d1f56c798d6ea44537ba3",
- "Node name for S&R": "FunPackHybridEuler2SSampler"
- },
- "widgets_values": [
- 0,
- 0,
- 1,
- 0.37,
- 0.5,
- "off",
- 0,
- 1,
- 0.04,
- 0,
- "off",
- 0,
- "default",
- "any"
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 5270,
- "type": "FunPackDistilledFlowSampler",
- "pos": [
- -3019.3905294772053,
- 3801.7929781876383
- ],
- "size": [
- 306.7000030517578,
- 126
- ],
- "flags": {},
- "order": 15,
- "mode": 0,
- "inputs": [
- {
- "name": "sigmas",
- "shape": 7,
- "type": "SIGMAS",
- "link": 14655
- }
- ],
- "outputs": [
- {
- "name": "sampler",
- "type": "SAMPLER",
- "links": [
- 14691
- ]
- },
- {
- "name": "sigmas",
- "type": "SIGMAS",
- "links": [
- 14692
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "ef96bf5a7ee921413e3002331d9896b4f6a7bf68",
- "Node name for S&R": "FunPackDistilledFlowSampler"
- },
- "widgets_values": [
- 1,
- 3,
- 0.05
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 4989,
- "type": "PrimitiveFloat",
- "pos": [
- -4048.7135868346554,
- 4199.480708041753
- ],
- "size": [
- 503.47815868041744,
- 77.73054862088065
- ],
- "flags": {},
- "order": 6,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "FLOAT",
- "type": "FLOAT",
- "links": [
- 14066,
- 14088,
- 14331,
- 14598
- ]
- }
- ],
- "title": "fps",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.16.0",
- "Node name for S&R": "PrimitiveFloat"
- },
- "widgets_values": [
- 30
- ],
- "color": "#222",
- "bgcolor": "#000"
- },
- {
- "id": 4988,
- "type": "PrimitiveInt",
- "pos": [
- -4049.7571194831658,
- 4080.7000796267494
- ],
- "size": [
- 506.76658345056376,
- 85.28842477014678
- ],
- "flags": {},
- "order": 7,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "INT",
- "type": "INT",
- "links": [
- 14059
- ]
- }
- ],
- "title": "number of frames",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.16.0",
- "Node name for S&R": "PrimitiveInt"
- },
- "widgets_values": [
- 465,
- "fixed"
- ],
- "color": "#222",
- "bgcolor": "#000"
- },
- {
- "id": 5019,
- "type": "VHS_VideoCombine",
- "pos": [
- -2104.563681442465,
- 3489.2174507931663
- ],
- "size": [
- 250,
- 310
- ],
- "flags": {},
- "order": 39,
- "mode": 0,
- "inputs": [
- {
- "name": "images",
- "type": "IMAGE",
- "link": 14011
- },
- {
- "name": "audio",
- "shape": 7,
- "type": "AUDIO",
- "link": 14285
- },
- {
- "name": "meta_batch",
- "shape": 7,
- "type": "VHS_BatchManager",
- "link": null
- },
- {
- "name": "vae",
- "shape": 7,
- "type": "VAE",
- "link": null
- },
- {
- "name": "frame_rate",
- "type": "FLOAT",
- "widget": {
- "name": "frame_rate"
- },
- "link": 14598
- }
- ],
- "outputs": [
- {
- "name": "Filenames",
- "type": "VHS_FILENAMES",
- "links": null
- }
- ],
- "title": "Result",
- "properties": {
- "cnr_id": "comfyui-videohelpersuite",
- "ver": "2984ec4c4b93292421888f38db74a5e8802a8ff8",
- "Node name for S&R": "VHS_VideoCombine",
- "aux_id": "Kosinkadink/ComfyUI-VideoHelperSuite"
- },
- "widgets_values": {
- "frame_rate": 30,
- "loop_count": 0,
- "filename_prefix": "result_%date:MM-dd_hh-mm-ss%",
- "format": "video/h265-mp4",
- "pix_fmt": "yuv420p",
- "crf": 20,
- "save_metadata": false,
- "pingpong": false,
- "save_output": false,
- "videopreview": {
- "hidden": false,
- "paused": false,
- "params": {
- "filename": "result_05-16_04-27-03_00001-audio.mp4",
- "subfolder": "",
- "type": "temp",
- "format": "video/h265-mp4",
- "frame_rate": 30,
- "workflow": "result_05-16_04-27-03_00001.png",
- "fullpath": "/workspace/ComfyUI/temp/result_05-16_04-27-03_00001-audio.mp4"
- }
- }
- },
- "color": "#222",
- "bgcolor": "#000"
- },
- {
- "id": 2004,
- "type": "LoadImage",
- "pos": [
- -4386.856601914213,
- 4032.549991720392
- ],
- "size": [
- 280,
- 315
- ],
- "flags": {},
- "order": 8,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "IMAGE",
- "type": "IMAGE",
- "links": [
- 11002,
- 14062,
- 14280
- ]
- },
- {
- "name": "MASK",
- "type": "MASK",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.3.56",
- "Node name for S&R": "LoadImage"
- },
- "widgets_values": [
- "example.png",
- "image"
- ],
- "color": "#232",
- "bgcolor": "#353"
- },
- {
- "id": 5269,
- "type": "PrimitiveStringMultiline",
- "pos": [
- -3015.8419066275333,
- 4408.4844523717475
- ],
- "size": [
- 293.613770302838,
- 300.45069018781396
- ],
- "flags": {},
- "order": 9,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": [
- 14678
- ]
- }
- ],
- "title": "USER FEEDBACK",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.21.1",
- "Node name for S&R": "PrimitiveStringMultiline"
- },
- "widgets_values": [
- ""
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5188,
- "type": "c758fc63-b71c-43f2-b085-ded32443f756",
- "pos": [
- -4051.2856108495703,
- 3306.4096732268413
- ],
- "size": [
- 505.0919753788826,
- 132
- ],
- "flags": {
- "collapsed": false
- },
- "order": 18,
- "mode": 0,
- "inputs": [
- {
- "name": "image",
- "shape": 7,
- "type": "IMAGE",
- "link": 14280
- },
- {
- "name": "prompt",
- "type": "STRING",
- "widget": {
- "name": "prompt"
- },
- "link": 14281
- }
- ],
- "outputs": [
- {
- "name": "generated_text",
- "type": "STRING",
- "links": [
- 14282,
- 14492,
- 14715
- ]
- },
- {
- "label": "advisor_clip",
- "name": "CLIP",
- "type": "CLIP",
- "links": []
- }
- ],
- "properties": {
- "proxyWidgets": [
- [
- "5215",
- "prompt"
- ],
- [
- "5185",
- "seed"
- ]
- ],
- "cnr_id": "comfy-core",
- "ver": "0.20.1"
- },
- "widgets_values": [],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5283,
- "type": "FunPackAdvisorLLM",
- "pos": [
- -3492.967630840759,
- 4090.089153754085
- ],
- "size": [
- 445.1430921634246,
- 92.52361927162201
- ],
- "flags": {},
- "order": 10,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "advisor_clip",
- "type": "CLIP",
- "links": [
- 14713
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "da643403815492b0ce34706c2c8aa2e1e90be897",
- "Node name for S&R": "FunPackAdvisorLLM"
- },
- "widgets_values": [
- "huihui-ai/Huihui-Qwen3-8B-abliterated-v2",
- "bfloat16"
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5265,
- "type": "PreviewAny",
- "pos": [
- -2705.7957798201787,
- 4401.3057737819045
- ],
- "size": [
- 590.2691602795035,
- 308.2768435290318
- ],
- "flags": {},
- "order": 32,
- "mode": 0,
- "inputs": [
- {
- "name": "source",
- "type": "*",
- "link": 14688
- }
- ],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": null
- }
- ],
- "title": "Encoded prompts",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.21.1",
- "Node name for S&R": "PreviewAny"
- },
- "widgets_values": [
- null,
- null,
- null
- ]
- },
- {
- "id": 5271,
- "type": "FunPackDistilledFlowSampler",
- "pos": [
- -2404.5342868049584,
- 4111.793972001756
- ],
- "size": [
- 292.8115590208198,
- 126
- ],
- "flags": {},
- "order": 14,
- "mode": 0,
- "inputs": [
- {
- "name": "sigmas",
- "shape": 7,
- "type": "SIGMAS",
- "link": 14658
- }
- ],
- "outputs": [
- {
- "name": "sampler",
- "type": "SAMPLER",
- "links": [
- 14721
- ]
- },
- {
- "name": "sigmas",
- "type": "SIGMAS",
- "links": [
- 14722
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "ef96bf5a7ee921413e3002331d9896b4f6a7bf68",
- "Node name for S&R": "FunPackDistilledFlowSampler"
- },
- "widgets_values": [
- 1,
- 3,
- 0
- ],
- "color": "#233",
- "bgcolor": "#355"
- },
- {
- "id": 5012,
- "type": "PrimitiveStringMultiline",
- "pos": [
- -4056.7927927967858,
- 3480.098501565234
- ],
- "size": [
- 512.4831290916973,
- 134.94660216187458
- ],
- "flags": {
- "collapsed": false
- },
- "order": 11,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": [
- 14281,
- 14676,
- 14714
- ]
- }
- ],
- "title": "Positive Prompt",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.18.2",
- "Node name for S&R": "PrimitiveStringMultiline"
- },
- "widgets_values": [
- ""
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5256,
- "type": "PrimitiveStringMultiline",
- "pos": [
- -4051.3971026376844,
- 3652.749946157198
- ],
- "size": [
- 512.4831290916973,
- 134.94660216187458
- ],
- "flags": {
- "collapsed": false
- },
- "order": 12,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "STRING",
- "type": "STRING",
- "links": [
- 14716
- ]
- }
- ],
- "title": "Negative Prompt",
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.18.2",
- "Node name for S&R": "PrimitiveStringMultiline"
- },
- "widgets_values": [
- "music, dance, underwater, diving, slow motion, still image, bad quality, subtitles, text, watermark, overlay, pc game, yelling, console game, childish, ugly, text, blur, logo, wordmark, static, low quality, noise, white noise, bleep, censoring, censor, bleeping, beep, beeping, newscast, interview, podcast, chinese, mutant, horror, 70's, film grain, comedy, stand-up "
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5273,
- "type": "FunPackVideoRefinerV2",
- "pos": [
- -3494.571958333055,
- 3494.9190790425314
- ],
- "size": [
- 444.7284880493976,
- 552
- ],
- "flags": {},
- "order": 28,
- "mode": 0,
- "inputs": [
- {
- "name": "clip",
- "shape": 7,
- "type": "CLIP",
- "link": 14679
- },
- {
- "name": "advisor_clip",
- "shape": 7,
- "type": "CLIP",
- "link": 14713
- },
- {
- "name": "positive_conditioning",
- "shape": 7,
- "type": "CONDITIONING",
- "link": null
- },
- {
- "name": "lora_stack",
- "shape": 7,
- "type": "FUNPACK_LORA_STACK",
- "link": 14720
- },
- {
- "name": "clip_vision_output",
- "shape": 7,
- "type": "CLIP_VISION_OUTPUT",
- "link": null
- },
- {
- "name": "source_image",
- "shape": 7,
- "type": "IMAGE",
- "link": 14682
- },
- {
- "name": "model",
- "shape": 7,
- "type": "MODEL",
- "link": 14699
- },
- {
- "name": "refinement_key_input",
- "shape": 7,
- "type": "STRING",
- "link": 14683
- },
- {
- "name": "positive_prompt",
- "type": "STRING",
- "widget": {
- "name": "positive_prompt"
- },
- "link": 14719
- },
- {
- "name": "user_intent_prompt",
- "shape": 7,
- "type": "STRING",
- "widget": {
- "name": "user_intent_prompt"
- },
- "link": 14676
- },
- {
- "name": "feedback_prompt",
- "shape": 7,
- "type": "STRING",
- "widget": {
- "name": "feedback_prompt"
- },
- "link": 14678
- }
- ],
- "outputs": [
- {
- "name": "modified_positive",
- "type": "CONDITIONING",
- "links": [
- 14685
- ]
- },
- {
- "name": "status",
- "type": "STRING",
- "links": [
- 14686
- ]
- },
- {
- "name": "training_info",
- "type": "STRING",
- "links": [
- 14687
- ]
- },
- {
- "name": "loss_graph",
- "type": "IMAGE",
- "links": null
- },
- {
- "name": "encoded_prompts",
- "type": "STRING",
- "links": [
- 14688
- ]
- },
- {
- "name": "model",
- "type": "MODEL",
- "links": [
- 14694,
- 14695
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "d531576f8e1d537e753250c74a917c3b84c6a9d8",
- "Node name for S&R": "FunPackVideoRefinerV2"
- },
- "widgets_values": [
- "",
- "Perfect",
- "Refine",
- "Full",
- true,
- "randomize",
- false,
- true,
- true,
- true
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5239,
- "type": "FunPackRefinementKeyLoader",
- "pos": [
- -4043.589996529283,
- 4324.035253359687
- ],
- "size": [
- 314.7000030517578,
- 204
- ],
- "flags": {},
- "order": 13,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "name": "refinement_key",
- "type": "STRING",
- "links": [
- 14499,
- 14500,
- 14683,
- 14717
- ]
- },
- {
- "name": "status",
- "type": "STRING",
- "links": null
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "b5a875a2609e21f19c08e5951e72af2db220cb5c",
- "Node name for S&R": "FunPackRefinementKeyLoader",
- "aux_id": "digital-garbage/ComfyUI-FunPack"
- },
- "widgets_values": [
- "-None-",
- "example",
- true,
- "",
- "",
- ""
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5284,
- "type": "FunPackSceneBuilder",
- "pos": [
- -3494.5100293018086,
- 4227.7214500238215
- ],
- "size": [
- 442.82499885707193,
- 319.476380728378
- ],
- "flags": {},
- "order": 22,
- "mode": 0,
- "inputs": [
- {
- "name": "intent_prompt",
- "shape": 7,
- "type": "STRING",
- "link": 14714
- },
- {
- "name": "positive_prompt",
- "shape": 7,
- "type": "STRING",
- "link": 14715
- },
- {
- "name": "negative_prompt",
- "shape": 7,
- "type": "STRING",
- "link": 14716
- },
- {
- "name": "refinement_key_input",
- "shape": 7,
- "type": "STRING",
- "link": 14717
- },
- {
- "name": "lora_stack",
- "shape": 7,
- "type": "FUNPACK_LORA_STACK",
- "link": 14718
- }
- ],
- "outputs": [
- {
- "name": "positive_prompt",
- "type": "STRING",
- "links": [
- 14719
- ]
- },
- {
- "name": "negative_prompt",
- "type": "STRING",
- "links": [
- 14723
- ]
- },
- {
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "links": [
- 14720
- ]
- },
- {
- "name": "status",
- "type": "STRING",
- "links": null
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "da643403815492b0ce34706c2c8aa2e1e90be897",
- "Node name for S&R": "FunPackSceneBuilder"
- },
- "widgets_values": [
- "",
- "Manual",
- "-None-",
- "",
- "load",
- "",
- "",
- "",
- "{}",
- ""
- ],
- "color": "#432",
- "bgcolor": "#653"
- },
- {
- "id": 5259,
- "type": "CLIPTextEncode",
- "pos": [
- -3721.5111318318945,
- 4373.515625377411
- ],
- "size": [
- 400,
- 200
- ],
- "flags": {
- "collapsed": true
- },
- "order": 26,
- "mode": 0,
- "inputs": [
- {
- "name": "clip",
- "type": "CLIP",
- "link": 14593
- },
- {
- "name": "text",
- "type": "STRING",
- "widget": {
- "name": "text"
- },
- "link": 14723
- }
- ],
- "outputs": [
- {
- "name": "CONDITIONING",
- "type": "CONDITIONING",
- "links": [
- 14672
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.21.0",
- "Node name for S&R": "CLIPTextEncode"
- },
- "widgets_values": [
- ""
- ]
- },
- {
- "id": 5143,
- "type": "4a0d1ad0-44c5-466f-8153-4ad264f785ab",
- "pos": [
- -4387.679665836049,
- 3474.1287636541992
- ],
- "size": [
- 279.2243457043487,
- 513.7727601710899
- ],
- "flags": {},
- "order": 21,
- "mode": 0,
- "inputs": [
- {
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "link": 14645
- }
- ],
- "outputs": [
- {
- "label": "audio_vae",
- "name": "VAE",
- "type": "VAE",
- "links": [
- 14051,
- 14061,
- 14308
- ]
- },
- {
- "label": "fast_vae",
- "name": "VAE_1",
- "type": "VAE",
- "links": []
- },
- {
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "links": [
- 14718
- ]
- },
- {
- "label": "video_vae",
- "name": "VAE_2",
- "type": "VAE",
- "links": [
- 14055,
- 14060,
- 14300,
- 14309,
- 14325
- ]
- },
- {
- "label": "LTXV_model",
- "name": "model",
- "type": "MODEL",
- "links": [
- 14426
- ]
- },
- {
- "name": "CLIP",
- "type": "CLIP",
- "links": [
- 14593,
- 14679
- ]
- },
- {
- "name": "latent_downscale_factor",
- "type": "FLOAT",
- "links": []
- },
- {
- "label": "lora_loader_status",
- "name": "status",
- "type": "STRING",
- "links": [
- 14514
- ]
- }
- ],
- "properties": {
- "proxyWidgets": [],
- "cnr_id": "comfy-core",
- "ver": "0.19.5"
- },
- "widgets_values": [],
- "color": "#223",
- "bgcolor": "#335"
- },
- {
- "id": 5238,
- "type": "FunPackApplyLoraWeights",
- "pos": [
- -4872.344912331053,
- 3483.002463588519
- ],
- "size": [
- 443.64066986209673,
- 474
- ],
- "flags": {},
- "order": 20,
- "mode": 0,
- "inputs": [
- {
- "name": "refinement_key_input",
- "shape": 7,
- "type": "STRING",
- "link": 14499
- },
- {
- "name": "positive_prompt",
- "type": "STRING",
- "widget": {
- "name": "positive_prompt"
- },
- "link": 14492
- },
- {
- "name": "refinement_key",
- "type": "STRING",
- "widget": {
- "name": "refinement_key"
- },
- "link": 14500
- }
- ],
- "outputs": [
- {
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "links": [
- 14645
- ]
- },
- {
- "name": "status",
- "type": "STRING",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "02e89ebd36208f6f5c0d7780f89f7dcdc040b1f1",
- "Node name for S&R": "FunPackApplyLoraWeights",
- "aux_id": "digital-garbage/ComfyUI-FunPack"
- },
- "widgets_values": [
- "",
- "my_style_v1",
- "ltx2",
- true,
- "[]",
- {},
- {
- "on": false,
- "lora": "None",
- "type": "quality",
- "strength": 2
- },
- {
- "on": false,
- "lora": "None",
- "type": "action",
- "strength": 0.5
- },
- {
- "on": false,
- "lora": "None",
- "type": "action",
- "strength": 2
- },
- {
- "on": false,
- "lora": "None",
- "type": "general",
- "strength": 0.3
- },
- {
- "on": false,
- "lora": "None",
- "type": "quality",
- "strength": 0.7
- },
- {
- "on": false,
- "lora": "None",
- "type": "quality",
- "strength": 0.5
- },
- {
- "on": false,
- "lora": "None",
- "type": "action",
- "strength": 1
- },
- {
- "on": false,
- "lora": "None",
- "type": "style",
- "strength": 1
- },
- {
- "on": false,
- "lora": "None",
- "type": "quality",
- "strength": 1
- },
- {
- "on": false,
- "lora": "None",
- "type": "character",
- "strength": 1
- },
- ""
- ],
- "color": "#223",
- "bgcolor": "#335"
- }
- ],
- "links": [
- [
- 11002,
- 2004,
- 0,
- 2653,
- 0,
- "IMAGE"
- ],
- [
- 13455,
- 4845,
- 1,
- 5021,
- 0,
- "LATENT"
- ],
- [
- 13890,
- 5101,
- 0,
- 4845,
- 0,
- "LATENT"
- ],
- [
- 13963,
- 4528,
- 0,
- 5101,
- 5,
- "LATENT"
- ],
- [
- 14011,
- 5020,
- 0,
- 5019,
- 0,
- "IMAGE"
- ],
- [
- 14051,
- 5143,
- 0,
- 5021,
- 1,
- "VAE"
- ],
- [
- 14055,
- 5143,
- 3,
- 5020,
- 0,
- "VAE"
- ],
- [
- 14059,
- 4988,
- 0,
- 5144,
- 0,
- "INT"
- ],
- [
- 14060,
- 5143,
- 3,
- 5144,
- 1,
- "VAE"
- ],
- [
- 14061,
- 5143,
- 0,
- 5144,
- 2,
- "VAE"
- ],
- [
- 14062,
- 2004,
- 0,
- 5144,
- 3,
- "IMAGE"
- ],
- [
- 14064,
- 5144,
- 1,
- 4528,
- 1,
- "LATENT"
- ],
- [
- 14066,
- 4989,
- 0,
- 5145,
- 0,
- "FLOAT"
- ],
- [
- 14067,
- 5145,
- 0,
- 5144,
- 4,
- "INT"
- ],
- [
- 14088,
- 4989,
- 0,
- 5147,
- 2,
- "FLOAT"
- ],
- [
- 14278,
- 5147,
- 0,
- 5101,
- 1,
- "CONDITIONING"
- ],
- [
- 14279,
- 5147,
- 1,
- 5101,
- 2,
- "CONDITIONING"
- ],
- [
- 14280,
- 2004,
- 0,
- 5188,
- 0,
- "IMAGE"
- ],
- [
- 14281,
- 5012,
- 0,
- 5188,
- 1,
- "STRING"
- ],
- [
- 14282,
- 5188,
- 0,
- 5177,
- 0,
- "STRING"
- ],
- [
- 14285,
- 5021,
- 0,
- 5019,
- 1,
- "AUDIO"
- ],
- [
- 14297,
- 5195,
- 0,
- 5193,
- 5,
- "LATENT"
- ],
- [
- 14298,
- 4845,
- 0,
- 5192,
- 0,
- "LATENT"
- ],
- [
- 14300,
- 5143,
- 3,
- 5192,
- 2,
- "VAE"
- ],
- [
- 14301,
- 5196,
- 0,
- 5192,
- 1,
- "LATENT_UPSCALE_MODEL"
- ],
- [
- 14302,
- 4845,
- 1,
- 5195,
- 1,
- "LATENT"
- ],
- [
- 14303,
- 5193,
- 0,
- 5198,
- 0,
- "LATENT"
- ],
- [
- 14304,
- 5198,
- 0,
- 5199,
- 1,
- "LATENT"
- ],
- [
- 14305,
- 5199,
- 0,
- 5197,
- 0,
- "IMAGE"
- ],
- [
- 14306,
- 5198,
- 1,
- 5200,
- 0,
- "LATENT"
- ],
- [
- 14308,
- 5143,
- 0,
- 5200,
- 1,
- "VAE"
- ],
- [
- 14309,
- 5143,
- 3,
- 5199,
- 0,
- "VAE"
- ],
- [
- 14317,
- 5147,
- 0,
- 5193,
- 1,
- "CONDITIONING"
- ],
- [
- 14318,
- 5147,
- 1,
- 5193,
- 2,
- "CONDITIONING"
- ],
- [
- 14323,
- 5192,
- 0,
- 5205,
- 2,
- "LATENT"
- ],
- [
- 14324,
- 5205,
- 0,
- 5195,
- 0,
- "LATENT"
- ],
- [
- 14325,
- 5143,
- 3,
- 5205,
- 0,
- "VAE"
- ],
- [
- 14326,
- 5144,
- 2,
- 5205,
- 1,
- "IMAGE"
- ],
- [
- 14331,
- 4989,
- 0,
- 5197,
- 4,
- "FLOAT"
- ],
- [
- 14426,
- 5143,
- 4,
- 5229,
- 0,
- "MODEL"
- ],
- [
- 14450,
- 5200,
- 0,
- 5234,
- 0,
- "AUDIO"
- ],
- [
- 14451,
- 5234,
- 0,
- 5197,
- 1,
- "AUDIO"
- ],
- [
- 14480,
- 4845,
- 0,
- 5020,
- 1,
- "LATENT"
- ],
- [
- 14492,
- 5188,
- 0,
- 5238,
- 1,
- "STRING"
- ],
- [
- 14499,
- 5239,
- 0,
- 5238,
- 0,
- "STRING"
- ],
- [
- 14500,
- 5239,
- 0,
- 5238,
- 2,
- "STRING"
- ],
- [
- 14514,
- 5143,
- 7,
- 5168,
- 0,
- "STRING"
- ],
- [
- 14593,
- 5143,
- 5,
- 5259,
- 0,
- "CLIP"
- ],
- [
- 14598,
- 4989,
- 0,
- 5019,
- 4,
- "FLOAT"
- ],
- [
- 14600,
- 5144,
- 0,
- 4528,
- 0,
- "LATENT"
- ],
- [
- 14645,
- 5238,
- 0,
- 5143,
- 0,
- "FUNPACK_LORA_STACK"
- ],
- [
- 14655,
- 5050,
- 0,
- 5270,
- 0,
- "SIGMAS"
- ],
- [
- 14658,
- 5202,
- 0,
- 5271,
- 0,
- "SIGMAS"
- ],
- [
- 14666,
- 5050,
- 0,
- 5272,
- 0,
- "SIGMAS"
- ],
- [
- 14672,
- 5259,
- 0,
- 5147,
- 1,
- "CONDITIONING"
- ],
- [
- 14676,
- 5012,
- 0,
- 5273,
- 9,
- "STRING"
- ],
- [
- 14678,
- 5269,
- 0,
- 5273,
- 10,
- "STRING"
- ],
- [
- 14679,
- 5143,
- 5,
- 5273,
- 0,
- "CLIP"
- ],
- [
- 14682,
- 5144,
- 2,
- 5273,
- 5,
- "IMAGE"
- ],
- [
- 14683,
- 5239,
- 0,
- 5273,
- 7,
- "STRING"
- ],
- [
- 14685,
- 5273,
- 0,
- 5147,
- 0,
- "CONDITIONING"
- ],
- [
- 14686,
- 5273,
- 1,
- 5232,
- 0,
- "STRING"
- ],
- [
- 14687,
- 5273,
- 2,
- 5153,
- 0,
- "STRING"
- ],
- [
- 14688,
- 5273,
- 4,
- 5265,
- 0,
- "STRING"
- ],
- [
- 14691,
- 5270,
- 0,
- 5101,
- 3,
- "SAMPLER"
- ],
- [
- 14692,
- 5270,
- 1,
- 5101,
- 4,
- "SIGMAS"
- ],
- [
- 14694,
- 5273,
- 5,
- 5101,
- 0,
- "MODEL"
- ],
- [
- 14695,
- 5273,
- 5,
- 5193,
- 0,
- "MODEL"
- ],
- [
- 14699,
- 5229,
- 0,
- 5273,
- 6,
- "MODEL"
- ],
- [
- 14713,
- 5283,
- 0,
- 5273,
- 1,
- "CLIP"
- ],
- [
- 14714,
- 5012,
- 0,
- 5284,
- 0,
- "STRING"
- ],
- [
- 14715,
- 5188,
- 0,
- 5284,
- 1,
- "STRING"
- ],
- [
- 14716,
- 5256,
- 0,
- 5284,
- 2,
- "STRING"
- ],
- [
- 14717,
- 5239,
- 0,
- 5284,
- 3,
- "STRING"
- ],
- [
- 14718,
- 5143,
- 2,
- 5284,
- 4,
- "FUNPACK_LORA_STACK"
- ],
- [
- 14719,
- 5284,
- 0,
- 5273,
- 8,
- "STRING"
- ],
- [
- 14720,
- 5284,
- 2,
- 5273,
- 3,
- "FUNPACK_LORA_STACK"
- ],
- [
- 14721,
- 5271,
- 0,
- 5193,
- 3,
- "SAMPLER"
- ],
- [
- 14722,
- 5271,
- 1,
- 5193,
- 4,
- "SIGMAS"
- ],
- [
- 14723,
- 5284,
- 1,
- 5259,
- 1,
- "STRING"
- ]
- ],
- "floatingLinks": [
- {
- "id": 1,
- "origin_id": 4989,
- "origin_slot": 0,
- "target_id": -1,
- "target_slot": -1,
- "type": "FLOAT",
- "parentId": 23
- }
- ],
- "groups": [],
- "definitions": {
- "subgraphs": [
- {
- "id": "4a0d1ad0-44c5-466f-8153-4ad264f785ab",
- "version": 1,
- "state": {
- "lastGroupId": 151,
- "lastNodeId": 5284,
- "lastLinkId": 14723,
- "lastRerouteId": 57
- },
- "revision": 0,
- "config": {},
- "name": "Model Loaders",
- "inputNode": {
- "id": -10,
- "bounding": [
- -4325.009559761858,
- 4005.266954142581,
- 128,
- 68
- ]
- },
- "outputNode": {
- "id": -20,
- "bounding": [
- -3434.4839648526677,
- 3955.266954142581,
- 184.96665954589844,
- 208
- ]
- },
- "inputs": [
- {
- "id": "d0987976-3ded-4b5e-8cbb-bf7a8f81e33a",
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "linkIds": [
- 14026
- ],
- "localized_name": "lora_stack",
- "pos": [
- -4221.009559761858,
- 4029.266954142581
- ]
- }
- ],
- "outputs": [
- {
- "id": "5698eb50-25b5-43c3-bc6d-5ae6ed37cc69",
- "name": "VAE",
- "type": "VAE",
- "linkIds": [
- 13426
- ],
- "localized_name": "VAE",
- "label": "audio_vae",
- "pos": [
- -3410.4839648526677,
- 3979.266954142581
- ]
- },
- {
- "id": "93aec5ab-7fdc-4e97-8030-9b93ccb135f7",
- "name": "VAE_1",
- "type": "VAE",
- "linkIds": [
- 14032
- ],
- "localized_name": "VAE_1",
- "label": "fast_vae",
- "pos": [
- -3410.4839648526677,
- 3999.266954142581
- ]
- },
- {
- "id": "23e2004b-5760-4651-9808-e4759172761e",
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "linkIds": [
- 13861
- ],
- "localized_name": "lora_stack",
- "pos": [
- -3410.4839648526677,
- 4019.266954142581
- ]
- },
- {
- "id": "c70376ed-0ba0-4de8-8c69-d0b2e0f98c94",
- "name": "VAE_2",
- "type": "VAE",
- "linkIds": [
- 13428
- ],
- "localized_name": "VAE_2",
- "label": "video_vae",
- "pos": [
- -3410.4839648526677,
- 4039.266954142581
- ]
- },
- {
- "id": "d7031e7e-1171-43c2-9dd4-b1ea12834bbc",
- "name": "model",
- "type": "MODEL",
- "linkIds": [
- 14418
- ],
- "localized_name": "model",
- "label": "LTXV_model",
- "pos": [
- -3410.4839648526677,
- 4059.266954142581
- ]
- },
- {
- "id": "3024b60b-1ea8-474c-91cd-85317f7a67a3",
- "name": "CLIP",
- "type": "CLIP",
- "linkIds": [
- 13435
- ],
- "localized_name": "CLIP",
- "pos": [
- -3410.4839648526677,
- 4079.266954142581
- ]
- },
- {
- "id": "b5fb5099-9092-44f9-bacc-690a2860aa29",
- "name": "latent_downscale_factor",
- "type": "FLOAT",
- "linkIds": [],
- "pos": [
- -3410.4839648526677,
- 4099.266954142581
- ]
- },
- {
- "id": "cfe9f2c2-261d-43b8-8591-00161f7bd393",
- "name": "status",
- "type": "STRING",
- "linkIds": [
- 14188
- ],
- "label": "lora_loader_status",
- "pos": [
- -3410.4839648526677,
- 4119.266954142581
- ]
- }
- ],
- "widgets": [],
- "nodes": [
- {
- "id": 5104,
- "type": "FunPackLoraLoader",
- "pos": [
- -3840.6243036178707,
- 3669.1897293353245
- ],
- "size": [
- 230,
- 90
- ],
- "flags": {},
- "order": 5,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "model",
- "name": "model",
- "type": "MODEL",
- "link": 13916
- },
- {
- "localized_name": "lora_stack",
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "link": 14026
- },
- {
- "localized_name": "clip",
- "name": "clip",
- "shape": 7,
- "type": "CLIP",
- "link": null
- }
- ],
- "outputs": [
- {
- "localized_name": "MODEL",
- "name": "MODEL",
- "type": "MODEL",
- "links": [
- 14320
- ]
- },
- {
- "localized_name": "CLIP",
- "name": "CLIP",
- "type": "CLIP",
- "links": null
- },
- {
- "localized_name": "lora_stack",
- "name": "lora_stack",
- "type": "FUNPACK_LORA_STACK",
- "links": [
- 13861
- ]
- },
- {
- "localized_name": "status",
- "name": "status",
- "type": "STRING",
- "links": [
- 14188
- ]
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-FunPack",
- "ver": "707e27279e2be733a5d787496742848bc745c258",
- "Node name for S&R": "FunPackLoraLoader",
- "aux_id": "digital-garbage/ComfyUI-FunPack"
- },
- "widgets_values": []
- },
- {
- "id": 5002,
- "type": "DiffusionModelLoaderKJ",
- "pos": [
- -4141.903414758422,
- 4121.852190133855
- ],
- "size": [
- 312.06666564941406,
- 178
- ],
- "flags": {},
- "order": 4,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "extra_state_dict",
- "name": "extra_state_dict",
- "shape": 7,
- "type": "STRING",
- "link": null
- }
- ],
- "outputs": [
- {
- "localized_name": "MODEL",
- "name": "MODEL",
- "type": "MODEL",
- "links": [
- 13916
- ]
- }
- ],
- "title": "Load LTX-2.3 Diffusion Model",
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "52807058a17532dd3d03f2143fb4c52beab5f936",
- "Node name for S&R": "DiffusionModelLoaderKJ",
- "aux_id": "kijai/ComfyUI-KJNodes"
- },
- "widgets_values": [
- "ltx2.safetensors",
- "bf16",
- "bf16",
- false,
- "disabled",
- false
- ]
- },
- {
- "id": 5004,
- "type": "VAELoaderKJ",
- "pos": [
- -4142.837082457477,
- 3661.3251095596834
- ],
- "size": [
- 270,
- 106
- ],
- "flags": {},
- "order": 0,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "localized_name": "VAE",
- "name": "VAE",
- "type": "VAE",
- "links": [
- 13428
- ]
- }
- ],
- "title": "Load Video VAE",
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "52807058a17532dd3d03f2143fb4c52beab5f936",
- "Node name for S&R": "VAELoaderKJ",
- "aux_id": "kijai/ComfyUI-KJNodes"
- },
- "widgets_values": [
- "ltx2_video_vae.safetensors",
- "main_device",
- "fp32"
- ]
- },
- {
- "id": 5003,
- "type": "VAELoaderKJ",
- "pos": [
- -4145.009559761858,
- 3815.5795641379827
- ],
- "size": [
- 270,
- 106
- ],
- "flags": {},
- "order": 1,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "localized_name": "VAE",
- "name": "VAE",
- "type": "VAE",
- "links": [
- 13426
- ]
- }
- ],
- "title": "Load Audio VAE",
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "52807058a17532dd3d03f2143fb4c52beab5f936",
- "Node name for S&R": "VAELoaderKJ",
- "aux_id": "kijai/ComfyUI-KJNodes"
- },
- "widgets_values": [
- "ltx2_audio_vae.safetensors",
- "main_device",
- "fp32"
- ]
- },
- {
- "id": 5017,
- "type": "VAELoaderKJ",
- "pos": [
- -4143.886540579375,
- 3967.8256025029505
- ],
- "size": [
- 270,
- 106
- ],
- "flags": {},
- "order": 2,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "localized_name": "VAE",
- "name": "VAE",
- "type": "VAE",
- "links": [
- 14032
- ]
- }
- ],
- "title": "Load Preview VAE",
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "52807058a17532dd3d03f2143fb4c52beab5f936",
- "Node name for S&R": "VAELoaderKJ",
- "aux_id": "kijai/ComfyUI-KJNodes"
- },
- "widgets_values": [
- "taeltx2_3.safetensors",
- "main_device",
- "fp32"
- ]
- },
- {
- "id": 5006,
- "type": "DualCLIPLoader",
- "pos": [
- -3835.5303844223104,
- 3814.768391164597
- ],
- "size": [
- 270,
- 130
- ],
- "flags": {},
- "order": 3,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "localized_name": "CLIP",
- "name": "CLIP",
- "type": "CLIP",
- "links": [
- 13435
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.18.2",
- "Node name for S&R": "DualCLIPLoader"
- },
- "widgets_values": [
- "gemma.safetensors",
- "ltx2_embeddings_connector.safetensors",
- "ltxv",
- "default"
- ]
- },
- {
- "id": 5203,
- "type": "LTX2LoraLoaderAdvanced",
- "pos": [
- -4137.706720274676,
- 4359.203995818316
- ],
- "size": [
- 293.59999694824216,
- 242
- ],
- "flags": {},
- "order": 6,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "model",
- "name": "model",
- "type": "MODEL",
- "link": 14320
- },
- {
- "localized_name": "opt_lora_path",
- "name": "opt_lora_path",
- "shape": 7,
- "type": "STRING",
- "link": null
- },
- {
- "localized_name": "blocks",
- "name": "blocks",
- "shape": 7,
- "type": "SELECTEDDITBLOCKS",
- "link": null
- }
- ],
- "outputs": [
- {
- "localized_name": "model",
- "name": "model",
- "type": "MODEL",
- "links": [
- 14401
- ]
- },
- {
- "localized_name": "rank",
- "name": "rank",
- "type": "STRING",
- "links": null
- },
- {
- "localized_name": "loaded_keys_info",
- "name": "loaded_keys_info",
- "type": "STRING",
- "links": null
- }
- ],
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "eeb2ae222c1bddce91506f22a6a5110964a42ce4",
- "Node name for S&R": "LTX2LoraLoaderAdvanced",
- "aux_id": "kijai/ComfyUI-KJNodes"
- },
- "widgets_values": [
- "distilled_72.safetensors",
- 0.88,
- 1,
- 0,
- 0,
- 0,
- 1
- ]
- },
- {
- "id": 5204,
- "type": "LTX2LoraLoaderAdvanced",
- "pos": [
- -3821.7206559166707,
- 4361.663434778751
- ],
- "size": [
- 293.59999694824216,
- 242
- ],
- "flags": {},
- "order": 7,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "model",
- "name": "model",
- "type": "MODEL",
- "link": 14401
- },
- {
- "localized_name": "opt_lora_path",
- "name": "opt_lora_path",
- "shape": 7,
- "type": "STRING",
- "link": null
- },
- {
- "localized_name": "blocks",
- "name": "blocks",
- "shape": 7,
- "type": "SELECTEDDITBLOCKS",
- "link": null
- }
- ],
- "outputs": [
- {
- "localized_name": "model",
- "name": "model",
- "type": "MODEL",
- "links": [
- 14418
- ]
- },
- {
- "localized_name": "rank",
- "name": "rank",
- "type": "STRING",
- "links": null
- },
- {
- "localized_name": "loaded_keys_info",
- "name": "loaded_keys_info",
- "type": "STRING",
- "links": null
- }
- ],
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "eeb2ae222c1bddce91506f22a6a5110964a42ce4",
- "Node name for S&R": "LTX2LoraLoaderAdvanced",
- "aux_id": "kijai/ComfyUI-KJNodes"
- },
- "widgets_values": [
- "distilled_72.safetensors",
- 0.9,
- 0,
- 1,
- 1,
- 1,
- 1
- ]
- }
- ],
- "groups": [],
- "links": [
- {
- "id": 13916,
- "origin_id": 5002,
- "origin_slot": 0,
- "target_id": 5104,
- "target_slot": 0,
- "type": "MODEL"
- },
- {
- "id": 14026,
- "origin_id": -10,
- "origin_slot": 0,
- "target_id": 5104,
- "target_slot": 1,
- "type": "FUNPACK_LORA_STACK"
- },
- {
- "id": 13426,
- "origin_id": 5003,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 0,
- "type": "VAE"
- },
- {
- "id": 14032,
- "origin_id": 5017,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 1,
- "type": "VAE"
- },
- {
- "id": 13861,
- "origin_id": 5104,
- "origin_slot": 2,
- "target_id": -20,
- "target_slot": 2,
- "type": "FUNPACK_LORA_STACK"
- },
- {
- "id": 13428,
- "origin_id": 5004,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 3,
- "type": "VAE"
- },
- {
- "id": 13435,
- "origin_id": 5006,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 5,
- "type": "CLIP"
- },
- {
- "id": 14188,
- "origin_id": 5104,
- "origin_slot": 3,
- "target_id": -20,
- "target_slot": 7,
- "type": "STRING"
- },
- {
- "id": 14320,
- "origin_id": 5104,
- "origin_slot": 0,
- "target_id": 5203,
- "target_slot": 0,
- "type": "MODEL"
- },
- {
- "id": 14401,
- "origin_id": 5203,
- "origin_slot": 0,
- "target_id": 5204,
- "target_slot": 0,
- "type": "MODEL"
- },
- {
- "id": 14418,
- "origin_id": 5204,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 4,
- "type": "MODEL"
- }
- ],
- "extra": {}
- },
- {
- "id": "2d968b7e-61e9-49f7-a0a5-bb17386fa820",
- "version": 1,
- "state": {
- "lastGroupId": 151,
- "lastNodeId": 5284,
- "lastLinkId": 14723,
- "lastRerouteId": 57
- },
- "revision": 0,
- "config": {},
- "name": "Image Processing",
- "inputNode": {
- "id": -10,
- "bounding": [
- -4230.013931822186,
- 4280.664648566482,
- 120,
- 180
- ]
- },
- "outputNode": {
- "id": -20,
- "bounding": [
- -3392.3462803758225,
- 4300.664648566482,
- 120,
- 140
- ]
- },
- "inputs": [
- {
- "id": "0cd8eafb-4599-4caf-8fc4-30f63acf3cd8",
- "name": "length",
- "type": "INT",
- "linkIds": [
- 13389,
- 13390
- ],
- "localized_name": "length",
- "pos": [
- -4130.013931822186,
- 4300.664648566482
- ]
- },
- {
- "id": "45f7e533-25b2-4795-acd6-5dc96f4e70d5",
- "name": "vae",
- "type": "VAE",
- "linkIds": [
- 14054,
- 14327,
- 14351
- ],
- "localized_name": "vae",
- "pos": [
- -4130.013931822186,
- 4320.664648566482
- ]
- },
- {
- "id": "e7b3fa68-1694-4761-a92b-f9cf55c8b0d7",
- "name": "audio_vae",
- "type": "VAE",
- "linkIds": [
- 14050
- ],
- "localized_name": "audio_vae",
- "pos": [
- -4130.013931822186,
- 4340.664648566482
- ]
- },
- {
- "id": "5a5c6f1b-489a-4eff-95cc-ee5d159310eb",
- "name": "image",
- "type": "IMAGE,MASK",
- "linkIds": [
- 13474
- ],
- "localized_name": "image",
- "pos": [
- -4130.013931822186,
- 4360.664648566482
- ]
- },
- {
- "id": "9b785d25-1e6c-4255-bb3c-97aaf621afe2",
- "name": "frame_rate",
- "type": "INT",
- "linkIds": [
- 14065
- ],
- "pos": [
- -4130.013931822186,
- 4380.664648566482
- ]
- },
- {
- "id": "6db1bac4-138a-4b8b-90bf-3961f84d84a3",
- "name": "target_width",
- "type": "INT",
- "linkIds": [
- 14081,
- 14389
- ],
- "pos": [
- -4130.013931822186,
- 4400.664648566482
- ]
- },
- {
- "id": "0cd2e965-8222-4162-b16f-d204a10034f4",
- "name": "target_height",
- "type": "INT",
- "linkIds": [
- 14082,
- 14390
- ],
- "pos": [
- -4130.013931822186,
- 4420.664648566482
- ]
- }
- ],
- "outputs": [
- {
- "id": "42905067-8a71-4026-b386-cdbcbbf428d6",
- "name": "latent",
- "type": "LATENT",
- "linkIds": [
- 14353
- ],
- "localized_name": "latent",
- "label": "video_latent",
- "pos": [
- -3372.3462803758225,
- 4320.664648566482
- ]
- },
- {
- "id": "832cd750-af0e-4127-b7e0-9e7d87efa173",
- "name": "Latent",
- "type": "LATENT",
- "linkIds": [
- 13696
- ],
- "localized_name": "Latent",
- "label": "audio_latent",
- "pos": [
- -3372.3462803758225,
- 4340.664648566482
- ]
- },
- {
- "id": "70c112e1-3f52-4d3e-bff3-43133fe0f5a9",
- "name": "output_image",
- "type": "IMAGE",
- "linkIds": [
- 14149
- ],
- "pos": [
- -3372.3462803758225,
- 4360.664648566482
- ]
- },
- {
- "id": "75faa65c-faad-46bc-bf59-05203952150a",
- "name": "LATENT",
- "type": "LATENT",
- "linkIds": [
- 14349
- ],
- "pos": [
- -3372.3462803758225,
- 4380.664648566482
- ]
- },
- {
- "id": "136987d9-e5df-4234-88a8-f34dcf46a64c",
- "name": "output",
- "type": "IMAGE",
- "linkIds": [
- 14391
- ],
- "label": "plain_black",
- "pos": [
- -3372.3462803758225,
- 4400.664648566482
- ]
- }
- ],
- "widgets": [],
- "nodes": [
- {
- "id": 3980,
- "type": "LTXVEmptyLatentAudio",
- "pos": [
- -3752.2385283541353,
- 4421.631082925247
- ],
- "size": [
- 305.8394470214844,
- 106
- ],
- "flags": {
- "collapsed": false
- },
- "order": 4,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "audio_vae",
- "name": "audio_vae",
- "type": "VAE",
- "link": 14050
- },
- {
- "localized_name": "frames_number",
- "name": "frames_number",
- "type": "INT",
- "widget": {
- "name": "frames_number"
- },
- "link": 13390
- },
- {
- "localized_name": "frame_rate",
- "name": "frame_rate",
- "type": "INT",
- "widget": {
- "name": "frame_rate"
- },
- "link": 14065
- }
- ],
- "outputs": [
- {
- "localized_name": "Latent",
- "name": "Latent",
- "type": "LATENT",
- "links": [
- 13696
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.3.64",
- "Node name for S&R": "LTXVEmptyLatentAudio"
- },
- "widgets_values": [
- 121,
- 25,
- 1
- ]
- },
- {
- "id": 3059,
- "type": "EmptyLTXVLatentVideo",
- "pos": [
- -4054.604251439193,
- 4362.302545732084
- ],
- "size": [
- 210,
- 194
- ],
- "flags": {},
- "order": 1,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "width",
- "name": "width",
- "type": "INT",
- "widget": {
- "name": "width"
- },
- "link": 13478
- },
- {
- "localized_name": "height",
- "name": "height",
- "type": "INT",
- "widget": {
- "name": "height"
- },
- "link": 13479
- },
- {
- "localized_name": "length",
- "name": "length",
- "type": "INT",
- "widget": {
- "name": "length"
- },
- "link": 13389
- }
- ],
- "outputs": [
- {
- "localized_name": "LATENT",
- "name": "LATENT",
- "type": "LATENT",
- "links": [
- 11401,
- 14329
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.3.43",
- "Node name for S&R": "EmptyLTXVLatentVideo"
- },
- "widgets_values": [
- 512,
- 768,
- 121,
- 1
- ]
- },
- {
- "id": 5212,
- "type": "VAEEncode",
- "pos": [
- -3395.061084351483,
- 4221.605231546082
- ],
- "size": [
- 140,
- 46
- ],
- "flags": {},
- "order": 7,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "pixels",
- "name": "pixels",
- "type": "IMAGE",
- "link": 14350
- },
- {
- "localized_name": "vae",
- "name": "vae",
- "type": "VAE",
- "link": 14351
- }
- ],
- "outputs": [
- {
- "localized_name": "LATENT",
- "name": "LATENT",
- "type": "LATENT",
- "links": [
- 14349
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "VAEEncode"
- },
- "widgets_values": []
- },
- {
- "id": 5220,
- "type": "LoadImage",
- "pos": [
- -4052.9158842937736,
- 4610.945426788913
- ],
- "size": [
- 282.7333297729492,
- 314
- ],
- "flags": {},
- "order": 0,
- "mode": 4,
- "inputs": [],
- "outputs": [
- {
- "localized_name": "IMAGE",
- "name": "IMAGE",
- "type": "IMAGE",
- "links": [
- 14388
- ]
- },
- {
- "localized_name": "MASK",
- "name": "MASK",
- "type": "MASK",
- "links": null
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "LoadImage"
- },
- "widgets_values": [
- "plain-black-mobile-background-uya4pitjl3vdmatx-1490018872.jpg",
- "image"
- ]
- },
- {
- "id": 5219,
- "type": "ImageTransformKJ",
- "pos": [
- -3749.590944998398,
- 4574.8080683828575
- ],
- "size": [
- 462,
- 261
- ],
- "flags": {
- "collapsed": true
- },
- "order": 8,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "image",
- "name": "image",
- "type": "IMAGE,MASK",
- "link": 14388
- },
- {
- "localized_name": "mask",
- "name": "mask",
- "shape": 7,
- "type": "MASK",
- "link": null
- },
- {
- "localized_name": "target_width",
- "name": "target_width",
- "type": "INT",
- "widget": {
- "name": "target_width"
- },
- "link": 14389
- },
- {
- "localized_name": "target_height",
- "name": "target_height",
- "type": "INT",
- "widget": {
- "name": "target_height"
- },
- "link": 14390
- }
- ],
- "outputs": [
- {
- "localized_name": "output",
- "name": "output",
- "type": "IMAGE",
- "links": [
- 14391
- ]
- },
- {
- "localized_name": "output_mask",
- "name": "output_mask",
- "type": "MASK",
- "links": null
- },
- {
- "color_off": "transparent",
- "color_on": "transparent",
- "label": " ",
- "localized_name": "bbox",
- "name": "bbox",
- "shape": 6,
- "type": "BBOX",
- "links": null
- },
- {
- "color_off": "transparent",
- "color_on": "transparent",
- "label": " ",
- "localized_name": "bbox_mask",
- "name": "bbox_mask",
- "shape": 6,
- "type": "MASK",
- "links": null
- },
- {
- "label": "768 width",
- "localized_name": "width",
- "name": "width",
- "type": "INT",
- "links": []
- },
- {
- "label": "768 height",
- "localized_name": "height",
- "name": "height",
- "type": "INT",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "52807058a17532dd3d03f2143fb4c52beab5f936",
- "Node name for S&R": "ImageTransformKJ",
- "previewEnabled": true,
- "showRotationCross": false,
- "canvasSize": [
- 432,
- 324
- ],
- "aux_id": "kijai/ComfyUI-KJNodes",
- "fillColor": "#000000",
- "previewFile": "crop_preview_dd29a0.webp"
- },
- "widgets_values": [
- 768,
- 768,
- "lanczos",
- "keep_long_edge",
- 16,
- "disabled",
- "disabled",
- "{\"fillColor\":\"#000000\"}",
- ""
- ]
- },
- {
- "id": 3336,
- "type": "LTXVPreprocess",
- "pos": [
- -4051.7822649723703,
- 4222.285074743058
- ],
- "size": [
- 270,
- 74
- ],
- "flags": {},
- "order": 3,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "image",
- "name": "image",
- "type": "IMAGE",
- "link": 13864
- }
- ],
- "outputs": [
- {
- "localized_name": "output_image",
- "name": "output_image",
- "type": "IMAGE",
- "links": [
- 13865,
- 14149,
- 14328,
- 14350
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.3.60",
- "Node name for S&R": "LTXVPreprocess"
- },
- "widgets_values": [
- 0
- ]
- },
- {
- "id": 5206,
- "type": "LTXVImgToVideoInplace",
- "pos": [
- -3747.668725148193,
- 4054.017919247807
- ],
- "size": [
- 270,
- 122
- ],
- "flags": {},
- "order": 6,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "vae",
- "name": "vae",
- "type": "VAE",
- "link": 14327
- },
- {
- "localized_name": "image",
- "name": "image",
- "type": "IMAGE",
- "link": 14328
- },
- {
- "localized_name": "latent",
- "name": "latent",
- "type": "LATENT",
- "link": 14329
- }
- ],
- "outputs": [
- {
- "localized_name": "latent",
- "name": "latent",
- "type": "LATENT",
- "links": [
- 14353
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "LTXVImgToVideoInplace"
- },
- "widgets_values": [
- 0.7,
- false
- ]
- },
- {
- "id": 3159,
- "type": "LTXVImgToVideoConditionOnly",
- "pos": [
- -3756.874428105229,
- 4233.494670882838
- ],
- "size": [
- 315.26666870117185,
- 142.859507731343
- ],
- "flags": {},
- "order": 2,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "vae",
- "name": "vae",
- "type": "VAE",
- "link": 14054
- },
- {
- "localized_name": "image",
- "name": "image",
- "type": "IMAGE",
- "link": 13865
- },
- {
- "localized_name": "latent",
- "name": "latent",
- "type": "LATENT",
- "link": 11401
- }
- ],
- "outputs": [
- {
- "localized_name": "latent",
- "name": "latent",
- "type": "LATENT",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI-LTXVideo",
- "ver": "d43181d5723c1721692baef01d90cd1a825bba91",
- "Node name for S&R": "LTXVImgToVideoConditionOnly",
- "aux_id": "Lightricks/ComfyUI-LTXVideo"
- },
- "widgets_values": [
- 0.7,
- false
- ]
- },
- {
- "id": 5031,
- "type": "ImageTransformKJ",
- "pos": [
- -4044.0432045178136,
- 4176.011069650051
- ],
- "size": [
- 520,
- 261
- ],
- "flags": {
- "collapsed": true
- },
- "order": 5,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "image",
- "name": "image",
- "type": "IMAGE,MASK",
- "link": 13474
- },
- {
- "localized_name": "mask",
- "name": "mask",
- "shape": 7,
- "type": "MASK",
- "link": null
- },
- {
- "localized_name": "target_width",
- "name": "target_width",
- "type": "INT",
- "widget": {
- "name": "target_width"
- },
- "link": 14081
- },
- {
- "localized_name": "target_height",
- "name": "target_height",
- "type": "INT",
- "widget": {
- "name": "target_height"
- },
- "link": 14082
- }
- ],
- "outputs": [
- {
- "localized_name": "output",
- "name": "output",
- "type": "IMAGE,MASK",
- "links": [
- 13864
- ]
- },
- {
- "localized_name": "output_mask",
- "name": "output_mask",
- "type": "MASK",
- "links": null
- },
- {
- "color_off": "transparent",
- "color_on": "transparent",
- "label": " ",
- "localized_name": "bbox",
- "name": "bbox",
- "shape": 6,
- "type": "BBOX",
- "links": null
- },
- {
- "color_off": "transparent",
- "color_on": "transparent",
- "label": " ",
- "localized_name": "bbox_mask",
- "name": "bbox_mask",
- "shape": 6,
- "type": "MASK",
- "links": null
- },
- {
- "label": "768 width",
- "localized_name": "width",
- "name": "width",
- "type": "INT",
- "links": [
- 13478
- ]
- },
- {
- "label": "768 height",
- "localized_name": "height",
- "name": "height",
- "type": "INT",
- "links": [
- 13479
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfyui-kjnodes",
- "ver": "52807058a17532dd3d03f2143fb4c52beab5f936",
- "Node name for S&R": "ImageTransformKJ",
- "previewEnabled": true,
- "showRotationCross": false,
- "canvasSize": [
- 768,
- 768
- ],
- "aux_id": "kijai/ComfyUI-KJNodes",
- "fillColor": "#000000"
- },
- "widgets_values": [
- 768,
- 768,
- "lanczos",
- "keep_long_edge",
- 16,
- "disabled",
- "disabled",
- "{\"fillColor\":\"#000000\"}",
- ""
- ]
- }
- ],
- "groups": [],
- "links": [
- {
- "id": 13478,
- "origin_id": 5031,
- "origin_slot": 4,
- "target_id": 3059,
- "target_slot": 0,
- "type": "INT"
- },
- {
- "id": 13479,
- "origin_id": 5031,
- "origin_slot": 5,
- "target_id": 3059,
- "target_slot": 1,
- "type": "INT"
- },
- {
- "id": 13865,
- "origin_id": 3336,
- "origin_slot": 0,
- "target_id": 3159,
- "target_slot": 1,
- "type": "IMAGE"
- },
- {
- "id": 11401,
- "origin_id": 3059,
- "origin_slot": 0,
- "target_id": 3159,
- "target_slot": 2,
- "type": "LATENT"
- },
- {
- "id": 13864,
- "origin_id": 5031,
- "origin_slot": 0,
- "target_id": 3336,
- "target_slot": 0,
- "type": "IMAGE"
- },
- {
- "id": 13389,
- "origin_id": -10,
- "origin_slot": 0,
- "target_id": 3059,
- "target_slot": 2,
- "type": "INT"
- },
- {
- "id": 13390,
- "origin_id": -10,
- "origin_slot": 0,
- "target_id": 3980,
- "target_slot": 1,
- "type": "INT"
- },
- {
- "id": 14054,
- "origin_id": -10,
- "origin_slot": 1,
- "target_id": 3159,
- "target_slot": 0,
- "type": "VAE"
- },
- {
- "id": 14050,
- "origin_id": -10,
- "origin_slot": 2,
- "target_id": 3980,
- "target_slot": 0,
- "type": "VAE"
- },
- {
- "id": 13474,
- "origin_id": -10,
- "origin_slot": 3,
- "target_id": 5031,
- "target_slot": 0,
- "type": "IMAGE,MASK"
- },
- {
- "id": 13696,
- "origin_id": 3980,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 1,
- "type": "LATENT"
- },
- {
- "id": 14065,
- "origin_id": -10,
- "origin_slot": 4,
- "target_id": 3980,
- "target_slot": 2,
- "type": "INT"
- },
- {
- "id": 14081,
- "origin_id": -10,
- "origin_slot": 5,
- "target_id": 5031,
- "target_slot": 2,
- "type": "INT"
- },
- {
- "id": 14082,
- "origin_id": -10,
- "origin_slot": 6,
- "target_id": 5031,
- "target_slot": 3,
- "type": "INT"
- },
- {
- "id": 14149,
- "origin_id": 3336,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 2,
- "type": "IMAGE"
- },
- {
- "id": 14327,
- "origin_id": -10,
- "origin_slot": 1,
- "target_id": 5206,
- "target_slot": 0,
- "type": "VAE"
- },
- {
- "id": 14328,
- "origin_id": 3336,
- "origin_slot": 0,
- "target_id": 5206,
- "target_slot": 1,
- "type": "IMAGE"
- },
- {
- "id": 14329,
- "origin_id": 3059,
- "origin_slot": 0,
- "target_id": 5206,
- "target_slot": 2,
- "type": "LATENT"
- },
- {
- "id": 14349,
- "origin_id": 5212,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 3,
- "type": "LATENT"
- },
- {
- "id": 14350,
- "origin_id": 3336,
- "origin_slot": 0,
- "target_id": 5212,
- "target_slot": 0,
- "type": "IMAGE"
- },
- {
- "id": 14351,
- "origin_id": -10,
- "origin_slot": 1,
- "target_id": 5212,
- "target_slot": 1,
- "type": "VAE"
- },
- {
- "id": 14353,
- "origin_id": 5206,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 0,
- "type": "LATENT"
- },
- {
- "id": 14388,
- "origin_id": 5220,
- "origin_slot": 0,
- "target_id": 5219,
- "target_slot": 0,
- "type": "IMAGE"
- },
- {
- "id": 14389,
- "origin_id": -10,
- "origin_slot": 5,
- "target_id": 5219,
- "target_slot": 2,
- "type": "INT"
- },
- {
- "id": 14390,
- "origin_id": -10,
- "origin_slot": 6,
- "target_id": 5219,
- "target_slot": 3,
- "type": "INT"
- },
- {
- "id": 14391,
- "origin_id": 5219,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 4,
- "type": "IMAGE"
- }
- ],
- "extra": {}
- },
- {
- "id": "c758fc63-b71c-43f2-b085-ded32443f756",
- "version": 1,
- "state": {
- "lastGroupId": 151,
- "lastNodeId": 5284,
- "lastLinkId": 14723,
- "lastRerouteId": 57
- },
- "revision": 0,
- "config": {},
- "name": "Enhancer",
- "inputNode": {
- "id": -10,
- "bounding": [
- -4185.975620413162,
- 2995.9229766057383,
- 128,
- 88
- ]
- },
- "outputNode": {
- "id": -20,
- "bounding": [
- -3519.1490771969034,
- 3006.5191220105444,
- 132.5999984741211,
- 88
- ]
- },
- "inputs": [
- {
- "id": "f3881fd2-d951-4f33-874c-a5cd7f3b1f7d",
- "name": "image",
- "type": "IMAGE",
- "linkIds": [
- 14378,
- 14478
- ],
- "localized_name": "image",
- "shape": 7,
- "pos": [
- -4081.975620413162,
- 3019.9229766057383
- ]
- },
- {
- "id": "0357b5d7-3e17-46f2-b7dc-62941f69b495",
- "name": "prompt",
- "type": "STRING",
- "linkIds": [
- 14463
- ],
- "localized_name": "prompt",
- "pos": [
- -4081.975620413162,
- 3039.9229766057383
- ]
- }
- ],
- "outputs": [
- {
- "id": "53386daa-e704-4fe0-b148-12f3a8138d5c",
- "name": "generated_text",
- "type": "STRING",
- "linkIds": [
- 14481
- ],
- "localized_name": "generated_text",
- "pos": [
- -3495.1490771969034,
- 3030.5191220105444
- ]
- },
- {
- "id": "e89cc53f-3283-48fc-8caf-831c82976ca6",
- "name": "CLIP",
- "type": "CLIP",
- "linkIds": [
- 14712
- ],
- "label": "advisor_clip",
- "pos": [
- -3495.1490771969034,
- 3050.5191220105444
- ]
- }
- ],
- "widgets": [],
- "nodes": [
- {
- "id": 5185,
- "type": "CR Seed",
- "pos": [
- -3933.9237459684864,
- 2701.3944678867133
- ],
- "size": [
- 270,
- 102
- ],
- "flags": {},
- "order": 0,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "localized_name": "seed",
- "name": "seed",
- "type": "INT",
- "links": [
- 14361,
- 14374
- ]
- },
- {
- "localized_name": "show_help",
- "name": "show_help",
- "type": "STRING",
- "links": null
- }
- ],
- "properties": {
- "cnr_id": "ComfyUI_Comfyroll_CustomNodes",
- "ver": "d78b780ae43fcf8c6b7c6505e6ffb4584281ceca",
- "Node name for S&R": "CR Seed",
- "aux_id": "Suzie1/ComfyUI_Comfyroll_CustomNodes"
- },
- "widgets_values": [
- 918462792992690,
- "randomize"
- ]
- },
- {
- "id": 5187,
- "type": "TextGenerateLTX2Prompt",
- "pos": [
- -3974.8004741920977,
- 3024.673944128882
- ],
- "size": [
- 400,
- 412
- ],
- "flags": {},
- "order": 2,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "clip",
- "name": "clip",
- "type": "CLIP",
- "link": 14461
- },
- {
- "localized_name": "image",
- "name": "image",
- "shape": 7,
- "type": "IMAGE",
- "link": 14478
- },
- {
- "localized_name": "video",
- "name": "video",
- "shape": 7,
- "type": "IMAGE",
- "link": null
- },
- {
- "localized_name": "audio",
- "name": "audio",
- "shape": 7,
- "type": "AUDIO",
- "link": null
- },
- {
- "localized_name": "seed",
- "name": "sampling_mode.seed",
- "type": "INT",
- "widget": {
- "name": "sampling_mode.seed"
- },
- "link": 14374
- }
- ],
- "outputs": [
- {
- "localized_name": "generated_text",
- "name": "generated_text",
- "type": "STRING",
- "links": []
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "TextGenerateLTX2Prompt"
- },
- "widgets_values": [
- "",
- 2048,
- "on",
- 0.7,
- 80,
- 1,
- 0.05,
- 1.05,
- 0,
- 0,
- false,
- true
- ]
- },
- {
- "id": 5215,
- "type": "TextGenerate",
- "pos": [
- -3970.984396111159,
- 3484.5373656987576
- ],
- "size": [
- 400,
- 412
- ],
- "flags": {},
- "order": 3,
- "mode": 0,
- "inputs": [
- {
- "localized_name": "clip",
- "name": "clip",
- "type": "CLIP",
- "link": 14706
- },
- {
- "localized_name": "image",
- "name": "image",
- "shape": 7,
- "type": "IMAGE",
- "link": 14378
- },
- {
- "localized_name": "video",
- "name": "video",
- "shape": 7,
- "type": "IMAGE",
- "link": null
- },
- {
- "localized_name": "audio",
- "name": "audio",
- "shape": 7,
- "type": "AUDIO",
- "link": null
- },
- {
- "localized_name": "prompt",
- "name": "prompt",
- "type": "STRING",
- "widget": {
- "name": "prompt"
- },
- "link": 14463
- },
- {
- "localized_name": "seed",
- "name": "sampling_mode.seed",
- "type": "INT",
- "widget": {
- "name": "sampling_mode.seed"
- },
- "link": 14361
- }
- ],
- "outputs": [
- {
- "localized_name": "generated_text",
- "name": "generated_text",
- "type": "STRING",
- "links": [
- 14481
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "TextGenerate"
- },
- "widgets_values": [
- "",
- 2048,
- "on",
- 0.7,
- 75,
- 1,
- 0.1,
- 1.05,
- 0,
- 0,
- false,
- true
- ]
- },
- {
- "id": 5183,
- "type": "CLIPLoader",
- "pos": [
- -3929.681384808283,
- 2867.7282445224428
- ],
- "size": [
- 270,
- 106
- ],
- "flags": {},
- "order": 1,
- "mode": 0,
- "inputs": [],
- "outputs": [
- {
- "localized_name": "CLIP",
- "name": "CLIP",
- "type": "CLIP",
- "links": [
- 14461,
- 14706,
- 14712
- ]
- }
- ],
- "properties": {
- "cnr_id": "comfy-core",
- "ver": "0.20.1",
- "Node name for S&R": "CLIPLoader"
- },
- "widgets_values": [
- "sulphur_enhance.safetensors",
- "stable_diffusion",
- "default"
- ]
- }
- ],
- "groups": [],
- "links": [
- {
- "id": 14361,
- "origin_id": 5185,
- "origin_slot": 0,
- "target_id": 5215,
- "target_slot": 5,
- "type": "INT"
- },
- {
- "id": 14374,
- "origin_id": 5185,
- "origin_slot": 0,
- "target_id": 5187,
- "target_slot": 4,
- "type": "INT"
- },
- {
- "id": 14378,
- "origin_id": -10,
- "origin_slot": 0,
- "target_id": 5215,
- "target_slot": 1,
- "type": "IMAGE"
- },
- {
- "id": 14461,
- "origin_id": 5183,
- "origin_slot": 0,
- "target_id": 5187,
- "target_slot": 0,
- "type": "CLIP"
- },
- {
- "id": 14463,
- "origin_id": -10,
- "origin_slot": 1,
- "target_id": 5215,
- "target_slot": 4,
- "type": "STRING"
- },
- {
- "id": 14478,
- "origin_id": -10,
- "origin_slot": 0,
- "target_id": 5187,
- "target_slot": 1,
- "type": "IMAGE"
- },
- {
- "id": 14481,
- "origin_id": 5215,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 0,
- "type": "STRING"
- },
- {
- "id": 14706,
- "origin_id": 5183,
- "origin_slot": 0,
- "target_id": 5215,
- "target_slot": 0,
- "type": "CLIP"
- },
- {
- "id": 14712,
- "origin_id": 5183,
- "origin_slot": 0,
- "target_id": -20,
- "target_slot": 1,
- "type": "CLIP"
- }
- ],
- "extra": {}
- }
- ]
- },
- "config": {},
- "extra": {
- "frontendVersion": "1.43.18",
- "VHS_latentpreview": false,
- "VHS_latentpreviewrate": 0,
- "VHS_MetadataImage": true,
- "VHS_KeepIntermediate": true,
- "workspace_info": {
- "id": "elBQFQknIoLYTEwIloQuw"
- },
- "node_versions": {
- "comfy-core": "0.3.30",
- "comfyui-kjnodes": "1.0.8"
- },
- "workflowRendererVersion": "LG",
- "reroutes": [
- {
- "id": 23,
- "pos": [
- -2790.427677365805,
- 3993.7794771888416
- ],
- "linkIds": [],
- "floating": {
- "slotType": "output"
- }
- }
- ],
- "workflow_name": "LTX-2.3 FunPack Node Set Example",
- "ds": {
- "scale": 0.41301848244687944,
- "offset": [
- 5117.186845626395,
- -2722.046614212112
- ]
- },
- "linkExtensions": []
- },
- "version": 0.4
-}
diff --git a/funpack.py b/funpack.py
index 53c1668e..d5d8f0af 100644
--- a/funpack.py
+++ b/funpack.py
@@ -18,7 +18,7 @@ try:
)
from .model_management import FunPackApplyLoraWeights, FunPackLoraLoader
from .samplers import FunPackHybridEuler2SSampler, FunPackLTXAVSceneChainSampler, sample_funpack_hybrid_euler_2s
- from .templates import FunPackRefinementKeyLoader, FunPackSceneBuilder
+ from .templates import FunPackRefinementKeyLoader
except ImportError:
from conditioning import (
FunPackLorebookEnhancer,
@@ -36,7 +36,7 @@ except ImportError:
)
from model_management import FunPackApplyLoraWeights, FunPackLoraLoader
from samplers import FunPackHybridEuler2SSampler, FunPackLTXAVSceneChainSampler, sample_funpack_hybrid_euler_2s
- from templates import FunPackRefinementKeyLoader, FunPackSceneBuilder
+ from templates import FunPackRefinementKeyLoader
NODE_CLASS_MAPPINGS = {
"FunPackPromptCombiner": FunPackPromptCombiner,
@@ -54,7 +54,6 @@ except ImportError:
"FunPackApplyLoraWeights": FunPackApplyLoraWeights,
"FunPackLoraLoader": FunPackLoraLoader,
"FunPackRefinementKeyLoader": FunPackRefinementKeyLoader,
- "FunPackSceneBuilder": FunPackSceneBuilder,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -73,7 +72,6 @@ except ImportError:
"FunPackApplyLoraWeights": "FunPack Apply LoRA Weights",
"FunPackLoraLoader": "FunPack LoRA Loader",
"FunPackRefinementKeyLoader": "FunPack Refinement Key Loader",
- "FunPackSceneBuilder": "FunPack Scene Builder",
}
__all__ = [
@@ -94,6 +92,5 @@ __all__ = [
"FunPackApplyLoraWeights",
"FunPackLoraLoader",
"FunPackRefinementKeyLoader",
- "FunPackSceneBuilder",
"sample_funpack_hybrid_euler_2s",
]
diff --git a/templates.py b/templates.py
index 1b03d350..747fa0d9 100644
--- a/templates.py
+++ b/templates.py
@@ -31,9 +31,6 @@ TEMPLATE_NONE = "-None-"
REFINEMENT_KEY_NONE = "-None-"
SCENE_NONE = "-None-"
TEMPLATE_DB_VERSION = 1
-SCENE_DB_VERSION = 1
-SCENE_STATE_FIELD = "scene_builder"
-SCENE_CATEGORY_SOURCES = {"auto", "refiner", "user"}
WILDCARD_RE = re.compile(r"\{([^{}]*\|[^{}]*)\}")
SCENE_CATEGORIES = {
"negative": {"bad", "blurry", "worst", "low", "noise", "deformed", "artifact", "ugly", "broken"},
@@ -68,9 +65,6 @@ def template_store_path():
return os.path.join(template_store_dir(), "templates.json")
-def scene_store_path():
- return os.path.join(template_store_dir(), "scenes.json")
-
def shortcut_store_path():
return os.path.join(template_store_dir(), "shortcuts.json")
@@ -278,15 +272,6 @@ def template_names():
return [TEMPLATE_NONE] + names
-def empty_scene_db():
- return {
- "version": SCENE_DB_VERSION,
- "source": "ComfyUI-FunPack",
- "universal_memory": {},
- "scenes": {},
- }
-
-
def empty_shortcut_db():
return {
"version": 1,
@@ -295,18 +280,6 @@ def empty_shortcut_db():
}
-def normalize_scene_db(data):
- if not isinstance(data, dict):
- data = empty_scene_db()
- data.setdefault("version", SCENE_DB_VERSION)
- data.setdefault("source", "ComfyUI-FunPack")
- if not isinstance(data.get("universal_memory"), dict):
- data["universal_memory"] = {}
- if not isinstance(data.get("scenes"), dict):
- data["scenes"] = {}
- return data
-
-
def shortcut_key(value):
value = re.sub(r"[^\w'’.-]+", " ", str(value or "").strip().lower(), flags=re.UNICODE)
return re.sub(r"\s+", " ", value).strip()
@@ -665,410 +638,6 @@ def apply_prompt_shortcuts(text, seed=0, shortcut_db=None):
return expanded, applied
-def load_scene_db(refinement_key=""):
- key = normalize_refinement_key(refinement_key)
- if key:
- state, _, _ = load_refinement_key_state(key, create=False)
- if isinstance(state, dict):
- return normalize_scene_db(state.get(SCENE_STATE_FIELD, {}))
- return empty_scene_db()
-
- path = scene_store_path()
- if not os.path.exists(path):
- return empty_scene_db()
- try:
- with open(path, "r", encoding="utf-8") as file:
- data = json.load(file)
- except (json.JSONDecodeError, OSError, ValueError):
- return empty_scene_db()
-
- return normalize_scene_db(data)
-
-
-def save_scene_db(data, refinement_key=""):
- key = normalize_refinement_key(refinement_key)
- data = normalize_scene_db(data)
- if key:
- state, _, _ = load_refinement_key_state(key, create=True)
- if isinstance(state, dict):
- state[SCENE_STATE_FIELD] = data
- save_refinement_key_state(state, key)
- return
-
- os.makedirs(template_store_dir(), exist_ok=True)
- with open(scene_store_path(), "w", encoding="utf-8") as file:
- json.dump(data, file, indent=2, sort_keys=True)
-
-
-def scene_names(refinement_key=""):
- data = load_scene_db(refinement_key)
- names = sorted(
- name for name in data.get("scenes", {}).keys()
- if isinstance(name, str) and name.strip()
- )
- return [SCENE_NONE] + names
-
-
-def normalize_scene_name(value):
- value = str(value or "").strip()
- if not value or value == SCENE_NONE:
- return ""
- return value
-
-
-def normalize_scene_key(value):
- value = re.sub(r"[^\w'’]+", " ", str(value or "").strip().lower(), flags=re.UNICODE)
- return re.sub(r"\s+", " ", value).strip()
-
-
-def normalize_scene_text_spacing(value):
- text = re.sub(r"[ \t\r\f\v]+", " ", str(value or "")).strip()
- text = re.sub(r"\s+([,.])", r"\1", text)
- text = re.sub(r"([,.])([^\s,.])", r"\1 \2", text)
- return text.strip()
-
-
-def scene_token_words(text):
- return [
- token.lower()
- for token in re.findall(r"[\w'’.-]+", str(text or ""), flags=re.UNICODE)
- if any(char.isalpha() for char in token) and len(token.strip("._-")) >= 2
- ]
-
-
-def categorize_scene_phrase(text, source="positive"):
- if source == "negative":
- return "negative"
- words = set(scene_token_words(text))
- best = ("details", 0)
- for category, keywords in SCENE_CATEGORIES.items():
- if category == "negative":
- continue
- score = len(words & keywords)
- if score > best[1]:
- best = (category, score)
- return best[0]
-
-
-def extract_scene_phrases(text, source="positive", include_words=False):
- phrases = []
- seen = set()
- for raw in re.split(r"[,;.\n]+", normalize_scene_text_spacing(text)):
- clean = re.sub(r"\s+", " ", raw).strip(" ,;:.").strip()
- if len(clean) < 2:
- continue
- key = clean.lower()
- if key in seen:
- continue
- words = scene_token_words(clean)
- if not words:
- continue
- seen.add(key)
- phrases.append({
- "text": clean,
- "key": key,
- "source": source,
- "category": categorize_scene_phrase(clean, source),
- "tokens": words,
- })
- if include_words:
- for word in words:
- word_key = word.lower().strip("._-")
- if len(word_key) < 3 or word_key in SCENE_STOP_WORDS or word_key in seen:
- continue
- seen.add(word_key)
- phrases.append({
- "text": word_key,
- "key": word_key,
- "source": source,
- "category": categorize_scene_phrase(word_key, source),
- "tokens": [word_key],
- })
- return phrases
-
-
-def remember_scene_phrases(data, positive_prompt="", negative_prompt=""):
- memory = data.setdefault("universal_memory", {})
- changed = False
- for source, prompt in (("positive", positive_prompt), ("negative", negative_prompt)):
- for phrase in extract_scene_phrases(prompt, source, include_words=True):
- key = phrase["key"]
- current = memory.setdefault(key, {
- "text": phrase["text"],
- "source": source,
- "category": phrase["category"],
- "tokens": phrase["tokens"],
- "count": 0,
- "created_at": now_iso(),
- "category_source": "auto",
- "category_locked": False,
- })
- if not bool(current.get("category_locked")):
- current["text"] = current.get("text") or phrase["text"]
- current["source"] = source
- current["category"] = phrase["category"]
- current["tokens"] = phrase["tokens"]
- current["category_source"] = str(current.get("category_source") or "auto")
- current["count"] = int(current.get("count", 0) or 0) + 1
- current["updated_at"] = now_iso()
- changed = True
- return changed
-
-
-def scene_memory_items(data):
- memory = data.get("universal_memory", {}) if isinstance(data, dict) else {}
- items = []
- for key, item in memory.items():
- if not isinstance(item, dict):
- continue
- text = str(item.get("text") or key).strip()
- if not text:
- continue
- items.append({
- "text": text,
- "key": str(key),
- "source": item.get("source", "positive"),
- "category": item.get("category", "details"),
- "category_source": item.get("category_source", "auto"),
- "category_locked": bool(item.get("category_locked")),
- "tokens": item.get("tokens", scene_token_words(text)),
- "count": int(item.get("count", 0) or 0),
- "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()))
-
-
-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)
- wildcard = bool(item.get("wildcard")) or bool(str(item.get("wildcard_group") or "").strip())
- category_source = str(item.get("category_source") or "auto").strip().lower()
- if category_source not in SCENE_CATEGORY_SOURCES:
- category_source = "user" if bool(item.get("category_locked")) else "auto"
- category_locked = bool(item.get("category_locked")) or category_source == "user"
- memory[key] = {
- "text": text,
- "source": source,
- "category": category,
- "category_source": category_source,
- "category_locked": category_locked,
- "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": wildcard,
- }
- return memory
-
-
-def scene_memory_authority_changed(previous, current):
- if not isinstance(previous, dict) or not isinstance(current, dict):
- return True
- return any(
- str(previous.get(field, "")).strip().lower() != str(current.get(field, "")).strip().lower()
- for field in ("text", "source", "category")
- )
-
-
-def apply_scene_database_authority(previous_memory, incoming_memory):
- previous_memory = previous_memory if isinstance(previous_memory, dict) else {}
- incoming_memory = incoming_memory if isinstance(incoming_memory, dict) else {}
- resolved = {}
- for key, item in incoming_memory.items():
- if not isinstance(item, dict):
- continue
- current = dict(item)
- previous = previous_memory.get(key)
- if isinstance(previous, dict) and not scene_memory_authority_changed(previous, current):
- current["category_source"] = str(previous.get("category_source") or "auto")
- current["category_locked"] = bool(previous.get("category_locked"))
- else:
- current["category_source"] = "user"
- current["category_locked"] = True
- resolved[key] = current
- return resolved
-
-
-def normalize_scene_phrase_list(value):
- if isinstance(value, str):
- try:
- value = json.loads(value)
- except (json.JSONDecodeError, TypeError, ValueError):
- value = [value]
- if not isinstance(value, list):
- return []
- result = []
- seen = set()
- for item in value:
- text = item.get("text") if isinstance(item, dict) else item
- clean = normalize_scene_text_spacing(text).strip(" ,;:.").strip()
- key = clean.lower()
- if not clean or key in seen:
- continue
- seen.add(key)
- result.append(clean)
- return result
-
-
-def parse_scene_payload(value):
- if isinstance(value, dict):
- payload = value
- elif isinstance(value, str) and value.strip():
- try:
- payload = json.loads(value)
- except (json.JSONDecodeError, TypeError, ValueError):
- payload = {}
- else:
- payload = {}
- if not isinstance(payload, dict):
- payload = {}
- return {
- "positive_phrases": normalize_scene_phrase_list(payload.get("positive_phrases", [])),
- "negative_phrases": normalize_scene_phrase_list(payload.get("negative_phrases", [])),
- }
-
-
-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": []}
- return {
- "positive_phrases": normalize_scene_phrase_list(scene.get("positive_phrases", [])),
- "negative_phrases": normalize_scene_phrase_list(scene.get("negative_phrases", [])),
- }
-
-
-def scene_text_from_phrases(phrases):
- return ", ".join(normalize_scene_phrase_list(phrases))
-
-
-def scene_prompt_text(scene, text_field, phrases_field):
- if not isinstance(scene, dict):
- return ""
- if text_field in scene:
- return str(scene.get(text_field) or "")
- return scene_text_from_phrases(scene.get(phrases_field, []))
-
-
-def scene_from_texts(name, aliases, output_mode, positive_text, negative_text, refinement_key=""):
- positive_text = normalize_scene_text_spacing(positive_text)
- negative_text = normalize_scene_text_spacing(negative_text)
- 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()
- 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:
- seen.add(key)
- aliases.append(clean)
- return aliases
-
-
-def scene_exact_match(intent_prompt, scenes):
- intent = normalize_scene_key(intent_prompt)
- if not intent:
- return "", None
- padded = f" {intent} "
- for name, scene in sorted(scenes.items(), key=lambda item: len(item[0]), reverse=True):
- keys = [name]
- if isinstance(scene, dict):
- keys.extend(scene.get("aliases", []) if isinstance(scene.get("aliases"), list) else [])
- for key in keys:
- clean = normalize_scene_key(key)
- if clean and f" {clean} " in padded:
- return name, scene
- return "", None
-
-
-def scene_fuzzy_match(intent_prompt, scenes):
- intent_words = set(scene_token_words(intent_prompt))
- if not intent_words:
- return "", None
- best = ("", None, 0.0)
- for name, scene in scenes.items():
- keys = [name]
- if isinstance(scene, dict):
- keys.extend(scene.get("aliases", []) if isinstance(scene.get("aliases"), list) else [])
- for key in keys:
- key_words = set(scene_token_words(key))
- if not key_words:
- continue
- overlap = len(intent_words & key_words)
- if overlap < max(1, len(key_words)):
- continue
- score = overlap / max(len(key_words), 1)
- if score > best[2]:
- best = (name, scene, score)
- return best[0], best[1]
-
-
-def find_scene_for_intent(intent_prompt, scenes):
- name, scene = scene_exact_match(intent_prompt, scenes)
- if name:
- return name, scene, "exact"
- name, scene = scene_fuzzy_match(intent_prompt, scenes)
- if name:
- return name, scene, "fuzzy"
- return "", None, "none"
-
-
-def scene_field_summary(scene):
- if not isinstance(scene, dict):
- return "none"
- fields = []
- 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"
-
-
def normalize_template_name(value):
value = str(value or "").strip()
if not value or value == TEMPLATE_NONE:
@@ -1147,57 +716,6 @@ 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
-
- wildcard_phrases = {}
- for item in scene_db.get("universal_memory", {}).values():
- if not isinstance(item, dict):
- continue
- wildcard = bool(item.get("wildcard")) or bool(str(item.get("wildcard_group") or "").strip())
- phrase = str(item.get("text") or "").strip()
- if not wildcard or not phrase:
- continue
- wildcard_phrases[normalize_scene_key(phrase)] = phrase
-
- if not wildcard_phrases:
- return text
-
- parts = re.split(r"([,;.\n]+)", text)
- 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 phrase_key and phrase_key in wildcard_phrases:
- run.append((index, phrase_key))
- else:
- flush_run()
- flush_run()
-
- 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())
@@ -1353,120 +871,6 @@ async def funpack_templates_import(request):
return web.json_response({"imported": imported, "templates": template_names()})
-@PromptServer.instance.routes.get("/funpack/scenes")
-async def funpack_scenes(request):
- key = normalize_refinement_key(request.query.get("key", ""))
- data = load_scene_db(key)
- return web.json_response(
- {
- "scenes": scene_names(key),
- "path": refinement_key_path(key) if key else scene_store_path(),
- "refinement_key": key,
- "data": data,
- "memory": scene_memory_items(data),
- },
- headers={"Cache-Control": "no-store, max-age=0"},
- )
-
-
-@PromptServer.instance.routes.get("/funpack/scenes/export")
-async def funpack_scenes_export(request):
- key = normalize_refinement_key(request.query.get("key", ""))
- data = load_scene_db(key)
- return web.json_response(
- data,
- headers={
- "Cache-Control": "no-store, max-age=0",
- "Content-Disposition": f"attachment; filename=funpack_scenes_{key or 'global'}.json",
- },
- )
-
-
-@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)
- memory = apply_scene_database_authority(data.get("universal_memory", {}), memory)
- 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", ""))
- incoming = await request.json()
- if not isinstance(incoming, dict):
- return web.json_response({"error": "Imported file is not a scene database."}, status=400)
-
- data = load_scene_db(key)
- imported_memory = incoming.get("universal_memory", {})
- if isinstance(imported_memory, dict):
- memory = data.setdefault("universal_memory", {})
- for memory_key, item in imported_memory.items():
- if isinstance(item, dict):
- memory[str(memory_key)] = item
-
- imported_scenes = incoming.get("scenes", {})
- if not isinstance(imported_scenes, dict):
- return web.json_response({"error": "Imported file does not contain a scenes object."}, status=400)
-
- scenes = data.setdefault("scenes", {})
- imported = 0
- for name, scene in imported_scenes.items():
- clean_name = normalize_scene_name(name)
- if not clean_name or not isinstance(scene, dict):
- continue
- item = dict(scene)
- item["name"] = clean_name
- item["updated_at"] = now_iso()
- scenes[clean_name] = item
- imported += 1
- save_scene_db(data, key)
- return web.json_response({"imported": imported, "scenes": scene_names(key), "refinement_key": key})
-
-
@PromptServer.instance.routes.get("/funpack/shortcuts")
async def funpack_shortcuts(_):
data = load_shortcut_db()
@@ -1833,254 +1237,3 @@ class FunPackRefinementKeyLoader:
return ("", "No refinement key selected. Pick an existing key or type a new key name.")
_, loaded_key, status = load_refinement_key_state(target, create=bool(create_if_missing))
return (loaded_key if loaded_key else target, status)
-
-
-class FunPackSceneBuilder:
- CATEGORY = "FunPack/Scene"
- RETURN_TYPES = ("STRING", "STRING", "FUNPACK_LORA_STACK", "STRING")
- RETURN_NAMES = (
- "positive_prompt",
- "negative_prompt",
- "lora_stack",
- "status",
- )
- FUNCTION = "build_scene"
- OUTPUT_NODE = True
- DESCRIPTION = "Builds named scene presets from manually selected universal prompt phrases."
-
- @classmethod
- def INPUT_TYPES(cls):
- return {
- "required": {
- "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"}),
- "scene_positive": ("STRING", {
- "default": "",
- "multiline": False,
- "placeholder": "Write or arrange positive scene words and phrases here.",
- }),
- "scene_negative": ("STRING", {
- "default": "",
- "multiline": False,
- "placeholder": "Write or arrange negative scene words and phrases here.",
- }),
- "refinement_key": ("STRING", {"default": "", "multiline": False}),
- "scene_payload": ("STRING", {"default": "{}", "multiline": False}),
- },
- "optional": {
- "intent_prompt": ("STRING", {
- "default": "",
- "multiline": True,
- "forceInput": True,
- "tooltip": "Connected intent text used for Auto scene detection.",
- }),
- "positive_prompt": ("STRING", {
- "default": "",
- "multiline": True,
- "forceInput": True,
- "tooltip": "Connected positive prompt used only to collect universal scene phrase memory.",
- }),
- "negative_prompt": ("STRING", {
- "default": "",
- "multiline": True,
- "forceInput": True,
- "tooltip": "Connected negative prompt used only to collect universal negative phrase memory.",
- }),
- "refinement_key_input": ("STRING", {
- "default": "",
- "multiline": False,
- "forceInput": True,
- "tooltip": "Optional linked refinement key, for example from FunPack Refinement Key Loader.",
- }),
- "lora_stack": ("FUNPACK_LORA_STACK", {
- "tooltip": "Optional current LoRA stack. Scene Builder passes it through unchanged so Refiner can use it for suggestions.",
- }),
- },
- }
-
- @classmethod
- def IS_CHANGED(cls, **kwargs):
- return float("nan")
-
- @classmethod
- def VALIDATE_INPUTS(cls, scene=None, **kwargs):
- return True
-
- 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
- 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 _shortcut_seed(self, refinement_key, prompt):
- key = f"{normalize_refinement_key(refinement_key)}::{str(prompt or '')}"
- return int(md5(key.encode("utf-8")).hexdigest()[:12], 16)
-
- def _shortcut_status(self, applied):
- if not applied:
- return "Shortcuts: none."
- labels = []
- for item in applied[:8]:
- labels.append(f"{item.get('trigger', '')}->{item.get('replacement', '')}")
- suffix = f", ... ({len(applied)} total)" if len(applied) > 8 else ""
- return f"Shortcuts: expanded {len(applied)} activation(s): {', '.join(labels)}{suffix}."
-
- def _outputs_for_scene(self, name, scene, scene_db=None, lora_stack=None, source="Manual"):
- positive = scene_prompt_text(scene, "positive_text", "positive_phrases")
- negative = scene_prompt_text(scene, "negative_text", "negative_phrases")
- positive, shortcut_applied = apply_prompt_shortcuts(
- positive,
- seed=self._shortcut_seed(scene.get("refinement_key", "") if isinstance(scene, dict) else "", positive),
- )
- 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'}'. "
- f"Stored fields: {scene_field_summary(scene)}.\n"
- f"Positive phrases: {len(scene.get('positive_phrases', []) or [])}. "
- f"Negative phrases: {len(scene.get('negative_phrases', []) or [])}. "
- f"LoRA stack pass-through: {lora_count} LoRA(s).\n"
- f"{self._shortcut_status(shortcut_applied)}"
- )
- return (positive, negative, lora_stack, status)
-
- def _learning_outputs(self, positive_prompt, negative_prompt, lora_stack=None, refinement_key=""):
- positive, shortcut_applied = apply_prompt_shortcuts(
- str(positive_prompt or ""),
- seed=self._shortcut_seed(refinement_key, positive_prompt),
- )
- negative = str(negative_prompt or "")
- lora_count = len(lora_stack.get("loras", [])) if isinstance(lora_stack, dict) else 0
- status = (
- "Scene Builder Learning: pass-through active. "
- f"Collected positive source: {'yes' if positive else 'no'}. "
- f"Collected negative source: {'yes' if negative else 'no'}. "
- f"LoRA stack pass-through: {lora_count} LoRA(s).\n"
- f"{self._shortcut_status(shortcut_applied)}"
- )
- return (positive, negative, lora_stack, status)
-
- def _empty_outputs(self, status):
- return ("", "", None, status)
-
- def build_scene(
- self,
- scene_name,
- 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="",
- lora_stack=None,
- output_mode=None,
- ):
- linked_key = normalize_refinement_key(refinement_key_input)
- if linked_key:
- refinement_key = linked_key
- selected_name = normalize_scene_name(scene)
- 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)
-
- data = load_scene_db(refinement_key)
- memory_positive = "\n".join(
- item for item in (
- positive_prompt,
- scene_positive,
- scene_text_from_phrases(payload.get("positive_phrases", [])),
- )
- if str(item or "").strip()
- )
- memory_negative = "\n".join(
- item for item in (
- negative_prompt,
- scene_negative,
- scene_text_from_phrases(payload.get("negative_phrases", [])),
- )
- if str(item or "").strip()
- )
- memory_changed = remember_scene_phrases(data, memory_positive, memory_negative)
- scenes = data.setdefault("scenes", {})
- manual = self._manual_scene(
- selected_name,
- scene_name,
- aliases,
- mode,
- scene_positive,
- scene_negative,
- refinement_key,
- payload,
- )
-
- if action == "delete":
- if not selected_name or selected_name not in scenes:
- if memory_changed:
- save_scene_db(data, refinement_key)
- return self._empty_outputs("Delete skipped: no selected scene.")
- deleted = scenes.pop(selected_name)
- save_scene_db(data, refinement_key)
- return self._empty_outputs(
- f"Deleted scene '{selected_name}'. Removed fields: {scene_field_summary(deleted)}."
- )
-
- if action == "save":
- target_name = normalize_scene_name(scene_name) or selected_name
- if not target_name:
- if memory_changed:
- save_scene_db(data, refinement_key)
- return self._empty_outputs("Save skipped: enter a scene name.")
- manual["name"] = target_name
- manual["created_at"] = scenes.get(target_name, {}).get("created_at", now_iso())
- manual["updated_at"] = now_iso()
- scenes[target_name] = manual
- save_scene_db(data, refinement_key)
- 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)
- if not target_name:
- if memory_changed:
- save_scene_db(data, refinement_key)
- return self._empty_outputs("Update skipped: select a scene or enter a scene name.")
- previous = scenes.get(target_name, {})
- manual["name"] = target_name
- manual["created_at"] = previous.get("created_at", now_iso()) if isinstance(previous, dict) else now_iso()
- manual["updated_at"] = now_iso()
- scenes[target_name] = manual
- save_scene_db(data, refinement_key)
- return self._outputs_for_scene(target_name, manual, data, lora_stack, "Manual updated")
-
- if mode == "Learning":
- if memory_changed:
- save_scene_db(data, refinement_key)
- return self._learning_outputs(positive_prompt, negative_prompt, lora_stack, refinement_key)
-
- 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, data, lora_stack, f"Auto {match_type}")
-
- if action == "load" and selected_name and selected_name in scenes and not scene_positive and not scene_negative:
- selected_scene = scenes.get(selected_name, {})
- if memory_changed:
- save_scene_db(data, refinement_key)
- return self._outputs_for_scene(selected_name, selected_scene, data, lora_stack, "Manual loaded")
-
- if memory_changed:
- save_scene_db(data, refinement_key)
- return self._outputs_for_scene(manual.get("name", ""), manual, data, lora_stack, "Manual")
diff --git a/tests/test_scene_builder.py b/tests/test_scene_builder.py
deleted file mode 100644
index 104939ad..00000000
--- a/tests/test_scene_builder.py
+++ /dev/null
@@ -1,1048 +0,0 @@
-import json
-import sys
-import types
-from pathlib import Path
-
-import torch
-
-sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
-
-
-class FakeRoutes:
- def get(self, _path):
- return lambda func: func
-
- def post(self, _path):
- return lambda func: func
-
-
-sys.modules.setdefault(
- "folder_paths",
- types.SimpleNamespace(get_user_directory=lambda: ""),
-)
-sys.modules.setdefault(
- "server",
- types.SimpleNamespace(PromptServer=types.SimpleNamespace(instance=types.SimpleNamespace(routes=FakeRoutes()))),
-)
-
-import templates
-from conditioning import FunPackVideoRefinerV2
-from templates import (
- FunPackSceneBuilder,
- apply_prompt_shortcuts,
- apply_scene_database_authority,
- extract_scene_phrases,
- load_shortcut_db,
- load_scene_db,
- load_transition_db,
- load_custom_transition_triggers,
- normalize_scene_text_spacing,
- normalize_scene_memory_items,
- normalize_shortcut_db,
- save_shortcut_db,
- remember_scene_phrases,
- save_shortcut_item,
- save_scene_db,
- save_transition_item,
- delete_transition_item,
- transition_items,
-)
-
-
-def use_tmp_scene_store(monkeypatch, tmp_path):
- monkeypatch.setattr(templates, "template_store_dir", lambda: str(tmp_path))
- monkeypatch.setattr(
- templates,
- "refinement_state_path",
- lambda key, namespace, prefix="refine": str(tmp_path / "refinements" / f"{prefix}_{namespace}_{key}.json"),
- )
- monkeypatch.setattr(
- FunPackVideoRefinerV2,
- "_v2_state_path",
- lambda self, key: str(tmp_path / "refinements" / f"refine_v2_clip_{key}.json"),
- )
-
-
-def test_scene_builder_collects_inputs_as_memory_but_outputs_selection(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
- payload = {
- "positive_phrases": ["red dress", "walking pose"],
- "negative_phrases": ["bad hands"],
- }
- input_lora_stack = {
- "version": 2,
- "mode": "ltx2",
- "per_block": True,
- "loras": [{"name": "vampire.safetensors", "model_weight": 0.8}],
- }
-
- positive, negative, lora_stack, status = builder.build_scene(
- scene="-None-",
- scene_name="Vampire Courtyard",
- aliases="gothic vampire",
- action="save",
- mode="Manual",
- intent_prompt="",
- positive_prompt="woman in courtyard, red dress, moonlight",
- negative_prompt="blurry, bad hands",
- refinement_key="scene_key",
- scene_payload=json.dumps(payload),
- lora_stack=input_lora_stack,
- )
-
- assert positive == "red dress, walking pose"
- assert negative == "bad hands"
- assert "moonlight" not in positive
- assert lora_stack is input_lora_stack
- assert "Manual saved" in status
-
- data = load_scene_db("scene_key")
- assert "woman in courtyard" in data["universal_memory"]
- assert "blurry" in data["universal_memory"]
- 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
- assert required["scene_positive"][1]["multiline"] is False
- assert required["scene_negative"][1]["multiline"] is False
-
-
-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_save_load_preserves_raw_prompt_order(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
- positive = "middle detail, first action. close-up camera, last environment, subject returns"
- negative = "blur, bad hands, wrong action"
-
- saved = builder.build_scene(
- scene="-None-",
- scene_name="Order Scene",
- aliases="",
- action="save",
- mode="Manual",
- scene_positive=positive,
- scene_negative=negative,
- positive_prompt="memory source only",
- negative_prompt="negative memory source only",
- refinement_key="order_key",
- )
-
- assert saved[0] == positive
- assert saved[1] == negative
- data = load_scene_db("order_key")
- scene = data["scenes"]["Order Scene"]
- assert scene["positive_text"] == positive
- assert scene["negative_text"] == negative
- assert scene["positive_phrases"] == [
- "middle detail",
- "first action",
- "close-up camera",
- "last environment",
- "subject returns",
- ]
-
- loaded = builder.build_scene(
- scene="Order Scene",
- scene_name="",
- aliases="",
- action="load",
- mode="Manual",
- scene_positive="",
- scene_negative="",
- refinement_key="order_key",
- )
-
- assert loaded[0] == positive
- assert loaded[1] == negative
- assert "Manual loaded" in loaded[3]
-
-
-def test_scene_builder_empty_saved_text_does_not_rebuild_from_phrase_arrays(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- data = load_scene_db("empty_text_key")
- data["scenes"]["Empty Positive"] = {
- "name": "Empty Positive",
- "aliases": [],
- "output_mode": "Manual",
- "positive_text": "",
- "negative_text": "",
- "positive_phrases": ["should not appear"],
- "negative_phrases": ["also should not appear"],
- }
- save_scene_db(data, "empty_text_key")
-
- outputs = FunPackSceneBuilder().build_scene(
- scene="Empty Positive",
- scene_name="",
- aliases="",
- action="load",
- mode="Manual",
- scene_positive="",
- scene_negative="",
- refinement_key="empty_text_key",
- )
-
- assert outputs[0] == ""
- assert outputs[1] == ""
-
-
-def test_scene_builder_auto_exact_and_fuzzy_match_saved_scene(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
- saved_payload = {
- "positive_phrases": ["anthro dragon", "mossy scales"],
- "negative_phrases": ["extra limbs"],
- }
- manual_payload = {
- "positive_phrases": ["manual fallback"],
- "negative_phrases": [],
- }
-
- builder.build_scene(
- scene="-None-",
- scene_name="Forest Dragon",
- aliases="woodland beast",
- action="save",
- mode="Manual",
- intent_prompt="",
- positive_prompt="anthro dragon, mossy scales",
- negative_prompt="extra limbs",
- refinement_key="dragon_key",
- scene_payload=json.dumps(saved_payload),
- )
-
- exact = builder.build_scene(
- scene="-None-",
- scene_name="",
- aliases="",
- action="load",
- mode="Auto",
- intent_prompt="please apply woodland beast now",
- positive_prompt="ignored prompt",
- negative_prompt="ignored negative",
- refinement_key="dragon_key",
- scene_payload=json.dumps(manual_payload),
- )
- assert exact[0] == "anthro dragon, mossy scales"
- assert exact[1] == "extra limbs"
- assert exact[2] is None
- assert "Auto exact" in exact[3]
-
- fuzzy = builder.build_scene(
- scene="-None-",
- scene_name="",
- aliases="",
- action="load",
- mode="Auto",
- intent_prompt="make it forest and dragon",
- positive_prompt="ignored prompt",
- negative_prompt="",
- refinement_key="dragon_key",
- scene_payload=json.dumps(manual_payload),
- )
- assert fuzzy[0] == "anthro dragon, mossy scales"
- assert "Auto fuzzy" in fuzzy[3]
-
-
-def test_scene_builder_auto_no_match_falls_back_to_manual(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
- payload = {
- "positive_phrases": ["manual only"],
- "negative_phrases": ["manual negative"],
- }
-
- outputs = builder.build_scene(
- scene="-None-",
- scene_name="",
- aliases="",
- action="load",
- mode="Auto",
- intent_prompt="no known scene here",
- positive_prompt="source memory only",
- negative_prompt="bad source memory",
- refinement_key="",
- scene_payload=json.dumps(payload),
- )
-
- assert outputs[0] == "manual only"
- assert outputs[1] == "manual negative"
- assert "Manual" in outputs[3]
-
-
-def test_scene_builder_auto_still_collects_provided_scene_knowledge(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
-
- builder.build_scene(
- scene="-None-",
- scene_name="",
- aliases="",
- action="load",
- mode="Auto",
- intent_prompt="no matching saved scene",
- scene_positive="glass rain, neon reflection",
- scene_negative="bad fingers",
- positive_prompt="",
- negative_prompt="",
- refinement_key="auto_memory_key",
- )
-
- data = load_scene_db("auto_memory_key")
- assert "glass rain" in data["universal_memory"]
- assert "neon reflection" in data["universal_memory"]
- assert "bad fingers" in data["universal_memory"]
-
-
-def test_scene_builder_learning_mode_collects_memory_and_passes_inputs_through(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
- lora_stack = {"loras": [{"name": "active.safetensors"}]}
- positive_prompt = " first prompt,\nsecond prompt "
- negative_prompt = "bad anatomy, blur"
-
- outputs = builder.build_scene(
- scene="-None-",
- scene_name="",
- aliases="",
- action="load",
- mode="Learning",
- intent_prompt="ignored scene trigger",
- positive_prompt=positive_prompt,
- negative_prompt=negative_prompt,
- refinement_key="learn_key",
- scene_payload=json.dumps({
- "positive_phrases": ["manual phrase"],
- "negative_phrases": ["manual negative"],
- }),
- lora_stack=lora_stack,
- )
-
- assert outputs[0] == positive_prompt
- assert outputs[1] == negative_prompt
- assert outputs[2] is lora_stack
- assert "Learning" in outputs[3]
-
- data = load_scene_db("learn_key")
- assert "first prompt" in data["universal_memory"]
- assert "second prompt" in data["universal_memory"]
- assert "bad anatomy" in data["universal_memory"]
- assert data["scenes"] == {}
-
-
-def test_scene_builder_learning_mode_ignores_selected_scene_output(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
-
- builder.build_scene(
- scene="-None-",
- scene_name="Saved Scene",
- aliases="",
- action="save",
- mode="Manual",
- scene_positive="saved positive",
- scene_negative="saved negative",
- positive_prompt="memory source",
- negative_prompt="",
- refinement_key="learn_selected_key",
- )
-
- outputs = builder.build_scene(
- scene="Saved Scene",
- scene_name="",
- aliases="",
- action="load",
- mode="Learning",
- positive_prompt="live positive",
- negative_prompt="live negative",
- refinement_key="learn_selected_key",
- )
-
- assert outputs[0] == "live positive"
- assert outputs[1] == "live negative"
- assert "saved positive" not in outputs[0]
- assert "Learning" in outputs[3]
-
-
-def test_scene_builder_auto_mode_ignores_selected_scene_when_intent_matches_other(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
-
- builder.build_scene(
- scene="-None-",
- scene_name="Manual Scene",
- aliases="manual scene",
- action="save",
- mode="Manual",
- scene_positive="manual selected",
- scene_negative="",
- refinement_key="auto_selected_key",
- )
- builder.build_scene(
- scene="-None-",
- scene_name="Auto Target",
- aliases="dragon alley",
- action="save",
- mode="Manual",
- scene_positive="auto matched",
- scene_negative="",
- refinement_key="auto_selected_key",
- )
-
- outputs = builder.build_scene(
- scene="Manual Scene",
- scene_name="",
- aliases="",
- action="load",
- mode="Auto",
- intent_prompt="show dragon alley",
- refinement_key="auto_selected_key",
- )
-
- assert outputs[0] == "auto matched"
- assert "Auto" in outputs[3]
-
-
-def test_scene_builder_collects_prompt_words_from_sentence_chunks(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
-
- builder.build_scene(
- scene="-None-",
- scene_name="",
- aliases="",
- action="load",
- mode="Learning",
- positive_prompt="person smoking in the rain",
- negative_prompt="",
- refinement_key="word_key",
- )
-
- data = load_scene_db("word_key")
- assert "person smoking in the rain" in data["universal_memory"]
- assert "person" in data["universal_memory"]
- assert "smoking" in data["universal_memory"]
- assert "the" not in data["universal_memory"]
-
-
-def test_scene_builder_treats_space_before_punctuation_as_normal_punctuation():
- assert normalize_scene_text_spacing("red dress . walking pose , smoke") == "red dress. walking pose, smoke"
- phrases = extract_scene_phrases("red dress . walking pose , smoke")
- assert [item["text"] for item in phrases] == ["red dress", "walking pose", "smoke"]
-
-
-def test_refiner_reset_preserves_scene_builder_memory_in_refinement_key(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- builder = FunPackSceneBuilder()
-
- builder.build_scene(
- scene="-None-",
- scene_name="Memory Scene",
- aliases="",
- action="save",
- mode="Manual",
- positive_prompt="silver hair, rain street",
- negative_prompt="blur",
- refinement_key="shared_key",
- scene_payload=json.dumps({
- "positive_phrases": ["silver hair"],
- "negative_phrases": ["blur"],
- }),
- )
-
- refiner = FunPackVideoRefinerV2()
- reset_state, reset_status = refiner._v2_load_state("shared_key", reset_session=True)
-
- assert reset_status == "fresh"
- assert reset_state["prompt_histories"] == {}
- 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_wildcards():
- memory = normalize_scene_memory_items([
- {"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"] is True
- assert memory["blue dress"]["wildcard"] is True
- assert memory["bad hands"]["wildcard"] is False
- assert memory["bad hands"]["source"] == "negative"
-
-
-def test_shortcuts_are_global_and_independent_from_refinement_keys(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
-
- save_shortcut_item({
- "name": "Run",
- "triggers": ["running"],
- "replacements": ["running freely"],
- })
-
- data = load_scene_db("some_key")
- assert data["scenes"] == {}
- shortcuts = load_shortcut_db()["shortcuts"]
- assert "run" in shortcuts
- assert shortcuts["run"]["triggers"] == ["running"]
-
-
-def test_refiner_reset_session_does_not_clear_global_shortcuts(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Run",
- "triggers": ["running"],
- "replacements": ["running freely"],
- })
-
- FunPackVideoRefinerV2()._v2_load_state("reset_key", reset_session=True)
-
- assert "run" in load_shortcut_db()["shortcuts"]
-
-
-def test_shortcuts_match_boundaries_and_preserve_surrounding_spacing(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Run",
- "triggers": ["running"],
- "replacements": ["running freely"],
- })
-
- expanded, applied = apply_prompt_shortcuts(
- "A video of man running in the park, running.",
- seed=1,
- )
-
- assert expanded == "A video of man running freely in the park, running freely."
- assert "manrunning" not in expanded
- assert "parkfreely" not in expanded
- assert len(applied) == 2
-
- unchanged, applied = apply_prompt_shortcuts("A video of over-running shoes", seed=1)
- assert unchanged == "A video of over-running shoes"
- assert applied == []
-
-
-def test_shortcuts_use_longest_trigger_and_seeded_replacement(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Look",
- "triggers": ["camera", "looks into camera"],
- "replacements": [
- "a man is facing the camera and looking into it",
- "a man's gaze is directed at the viewer",
- ],
- })
-
- first, first_applied = apply_prompt_shortcuts("He looks into camera.", seed=77)
- second, second_applied = apply_prompt_shortcuts("He looks into camera.", seed=77)
-
- assert first == second
- assert first_applied == second_applied
- assert first_applied[0]["trigger"] == "looks into camera"
- assert "He " in first
- assert first.endswith(".")
-
-
-def test_longer_phrase_wins_across_different_shortcuts(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Smoke",
- "triggers": ["smoking"],
- "replacements": ["pipe smoking"],
- })
- save_shortcut_item({
- "name": "Smoking Fish",
- "triggers": ["smoking fish"],
- "replacements": ["cold smoked salmon"],
- })
- save_shortcut_item({
- "name": "Chef Smoking Fish",
- "triggers": ["chef smoking fish"],
- "replacements": ["a chef preparing cold smoked salmon"],
- })
-
- # "smoking fish" wins over "smoking" alone
- result, applied = apply_prompt_shortcuts("I want smoking fish for dinner.", seed=1)
- assert result == "I want cold smoked salmon for dinner."
- assert len(applied) == 1
- assert applied[0]["trigger"] == "smoking fish"
-
- # "chef smoking fish" wins over both "smoking" and "smoking fish"
- result, applied = apply_prompt_shortcuts("chef smoking fish on a plate.", seed=1)
- assert result == "a chef preparing cold smoked salmon on a plate."
- assert len(applied) == 1
- assert applied[0]["trigger"] == "chef smoking fish"
-
- # standalone "smoking" not part of a longer phrase still applies
- result, applied = apply_prompt_shortcuts("smoking is shown on screen.", seed=1)
- assert result == "pipe smoking is shown on screen."
- assert len(applied) == 1
- assert applied[0]["trigger"] == "smoking"
-
- # mixed: standalone "smoking" and "smoking fish" each match their own shortcut
- result, applied = apply_prompt_shortcuts("smoking and smoking fish.", seed=1)
- assert result == "pipe smoking and cold smoked salmon."
- assert len(applied) == 2
- triggers = {a["trigger"] for a in applied}
- assert triggers == {"smoking", "smoking fish"}
-
-
-def test_empty_replacement_removes_phrase_and_cleans_artifacts(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Strip Game Tag",
- "triggers": ["from Zenless Zone Zero"],
- "replacements": [""],
- })
-
- # trailing removal - space+comma artifact cleaned
- result, applied = apply_prompt_shortcuts("Nicole from Zenless Zone Zero, fighting stance.", seed=1)
- assert result == "Nicole, fighting stance."
- assert len(applied) == 1
- assert applied[0]["replacement"] == ""
-
- # leading removal - leading comma+space cleaned
- result, applied = apply_prompt_shortcuts("from Zenless Zone Zero, Nicole in fighting stance.", seed=1)
- assert result == "Nicole in fighting stance."
-
- # middle removal - double space cleaned
- result, applied = apply_prompt_shortcuts("Nicole from Zenless Zone Zero and a sword.", seed=1)
- assert result == "Nicole and a sword."
-
- # end of string - trailing space cleaned
- result, applied = apply_prompt_shortcuts("Nicole from Zenless Zone Zero", seed=1)
- assert result == "Nicole"
-
-
-def test_disabled_shortcuts_do_not_expand(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Run",
- "enabled": False,
- "triggers": ["running"],
- "replacements": ["running freely"],
- })
-
- expanded, applied = apply_prompt_shortcuts("running", seed=1)
-
- assert expanded == "running"
- assert applied == []
-
-
-def test_shortcut_only_import_export_shape_round_trips(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- exported = normalize_shortcut_db({
- "shortcuts": {
- "look": {
- "name": "Look",
- "enabled": True,
- "triggers": ["looks into camera"],
- "replacements": ["a man is facing the camera and looking into it"],
- }
- }
- })
-
- save_shortcut_db(exported)
-
- loaded = load_shortcut_db()
- assert loaded["shortcuts"]["look"]["name"] == "Look"
- assert loaded["shortcuts"]["look"]["triggers"] == ["looks into camera"]
-
-
-def test_scene_builder_expands_shortcuts_only_in_positive_prompt(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Run",
- "triggers": ["running"],
- "replacements": ["running freely"],
- })
-
- positive, negative, _, status = FunPackSceneBuilder().build_scene(
- scene="-None-",
- scene_name="Shortcut Scene",
- aliases="",
- action="load",
- mode="Manual",
- scene_positive="A video of man running in the park",
- scene_negative="running",
- refinement_key="shortcut_key",
- )
-
- assert positive == "A video of man running freely in the park"
- assert negative == "running"
- assert "Shortcuts: expanded" in status
-
-
-def test_refiner_expands_positive_and_intent_shortcuts_before_encoding(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_shortcut_item({
- "name": "Run",
- "triggers": ["running"],
- "replacements": ["running freely"],
- })
-
- cond = [(torch.ones(1, 2, 4), {"pooled_output": torch.ones(1, 4)})]
- _, status, _, _, encoded_prompts, _, _ = FunPackVideoRefinerV2().refine_v2(
- "A video of man running",
- clip=None,
- rating="Missing action",
- refinement_key="refiner_shortcut_key",
- positive_conditioning=cond,
- user_intent_prompt="running",
- reset_session=True,
- )
-
- assert "A video of man running freely" in encoded_prompts
- assert "Shortcuts: expanded" in status
-
-
-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": 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")
-
- outputs = builder.build_scene(
- scene="-None-",
- scene_name="Wildcard Scene",
- aliases="",
- action="load",
- mode="Manual",
- scene_positive="red dress, blue dress, walking pose, green dress",
- scene_negative="",
- refinement_key="wild_key",
- )
-
- 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
-
-
-def test_scene_builder_database_authority_preserves_unchanged_rows():
- previous = normalize_scene_memory_items([
- {"text": "red dress", "category": "appearance", "category_source": "auto", "category_locked": False},
- ])
- incoming = normalize_scene_memory_items([
- {"text": "red dress", "category": "appearance"},
- ])
-
- resolved = apply_scene_database_authority(previous, incoming)
-
- assert resolved["red dress"]["category_source"] == "auto"
- assert resolved["red dress"]["category_locked"] is False
-
-
-def test_scene_builder_database_authority_locks_new_and_changed_rows():
- previous = normalize_scene_memory_items([
- {"text": "red dress", "category": "appearance"},
- {"text": "walking pose", "category": "action"},
- ])
- incoming = normalize_scene_memory_items([
- {"text": "red dress", "category": "style"},
- {"text": "blue dress", "category": "appearance"},
- ])
-
- resolved = apply_scene_database_authority(previous, incoming)
-
- assert resolved["red dress"]["category_source"] == "user"
- assert resolved["red dress"]["category_locked"] is True
- assert resolved["blue dress"]["category_source"] == "user"
- assert resolved["blue dress"]["category_locked"] is True
-
-
-def test_scene_builder_prompt_learning_keeps_locked_user_category():
- data = {
- "universal_memory": normalize_scene_memory_items([
- {"text": "running pose", "category": "appearance", "category_source": "user", "category_locked": True},
- ]),
- "scenes": {},
- }
-
- remember_scene_phrases(data, positive_prompt="running pose")
-
- item = data["universal_memory"]["running pose"]
- assert item["category"] == "appearance"
- assert item["category_source"] == "user"
- assert item["category_locked"] is True
- assert item["count"] == 1
-
-
-def test_refiner_scene_builder_sync_skips_locked_rows():
- refiner = FunPackVideoRefinerV2()
- state = {
- "scene_builder": {
- "universal_memory": normalize_scene_memory_items([
- {"text": "running pose", "category": "appearance", "category_source": "user", "category_locked": True},
- ]),
- "scenes": {},
- }
- }
- global_state = {"phrase_memory": {}}
- phrase = refiner._v2_classify_phrases(None, [{"text": "running pose", "tokens": ["running", "pose"]}])[0]
- last_run = {"prompt": "running pose", "phrases": [phrase]}
-
- refiner._v2_update_phrase_memory(
- global_state,
- last_run,
- {"key": "like", "reward": 1.0, "label": "Perfect"},
- 1,
- {"missing_axes": [], "satisfied_axes": ["action"], "resolved_axes": [], "regressed_axes": [], "wrong_axes": []},
- )
- refiner._v2_sync_scene_builder_memory(state, global_state, last_run, 1)
-
- item = state["scene_builder"]["universal_memory"]["running pose"]
- assert item["category"] == "appearance"
- assert item["category_source"] == "user"
- assert item["category_locked"] is True
-
-
-def test_custom_transition_save_load_delete(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_transition_item({"name": "Number One", "trigger": "1"})
- save_transition_item({"name": "Next Shot", "trigger": "NEXT"})
-
- db = load_transition_db()
- assert "number one" in db["transitions"]
- assert db["transitions"]["number one"]["trigger"] == "1"
- assert "replacement" not in db["transitions"]["number one"]
-
- items = transition_items()
- assert any(i["trigger"] == "1" for i in items)
-
- delete_transition_item("Number One")
- assert "number one" not in load_transition_db()["transitions"]
-
-
-def test_custom_transition_triggers_returns_enabled_only(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_transition_item({"name": "Cut", "trigger": "CUT", "enabled": True})
- save_transition_item({"name": "Disabled", "trigger": "SKIP", "enabled": False})
-
- triggers = load_custom_transition_triggers()
- assert "cut" in triggers # keys are lowercased for case-insensitive lookup
- assert "skip" not in triggers
-
-
-def test_split_prompt_recognizes_custom_trigger(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_transition_item({"name": "1", "trigger": "1"})
-
- refiner = FunPackVideoRefinerV2()
- segments = refiner._v2_split_prompt_by_transitions("beach waves, 1, mountain snow")
-
- assert len(segments) == 2
- assert segments[0].strip() == "beach waves"
- assert "mountain snow" in segments[1]
-
-
-def test_split_prompt_custom_trigger_kept_at_segment_start(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_transition_item({"name": "NEXT", "trigger": "NEXT"})
-
- refiner = FunPackVideoRefinerV2()
- segments = refiner._v2_split_prompt_by_transitions("beach waves, NEXT, mountain snow")
-
- assert len(segments) == 2
- assert "beach waves" in segments[0]
- assert segments[1].startswith("NEXT")
- assert "mountain snow" in segments[1]
-
-
-def test_split_prompt_drops_dangling_trailing_transition(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- refiner = FunPackVideoRefinerV2()
-
- # transition at the end with no real content after it
- segments = refiner._v2_split_prompt_by_transitions(
- "female character Kai'Sa, the scene then cuts to"
- )
- assert len(segments) == 1
- assert "Kai'Sa" in segments[0]
-
- # transition + lone article is still dangling - should also be dropped
- segments = refiner._v2_split_prompt_by_transitions(
- "female character Kai'Sa, the scene then cuts to the"
- )
- assert len(segments) == 1
- assert "Kai'Sa" in segments[0]
-
- # transition + real content after it must NOT be dropped
- segments = refiner._v2_split_prompt_by_transitions(
- "female character Kai'Sa, the scene then cuts to the beach"
- )
- assert len(segments) == 2
- assert "beach" in segments[1]
-
-
-def test_split_prompt_no_stray_comma_after_article(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- refiner = FunPackVideoRefinerV2()
-
- # "the scene then cuts to" matches, leaving "the, next view" as following - comma must be gone
- segments = refiner._v2_split_prompt_by_transitions(
- "Kai'Sa, the scene then cuts to the, next view"
- )
- assert len(segments) == 2
- assert "the, next" not in segments[1]
- assert "the next view" in segments[1]
-
-
-def test_split_prompt_does_not_split_on_scene_as_noun(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- refiner = FunPackVideoRefinerV2()
-
- # "scene proceeds/features/shows" must NOT trigger a split
- for phrase in ["scene proceeds", "scene features", "scene shows", "scene begins"]:
- text = f"Kai'Sa walking. The {phrase} into the next action."
- segments = refiner._v2_split_prompt_by_transitions(text)
- assert len(segments) == 1, f"Should not split on '{phrase}', got: {segments}"
-
- # numbered scene labels must still split
- segments = refiner._v2_split_prompt_by_transitions("Kai'Sa walking. scene 2 she runs.")
- assert len(segments) == 2
-
-
-def test_split_transition_placement_end(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- refiner = FunPackVideoRefinerV2()
-
- # placement=end: transition phrase closes the previous segment
- segments = refiner._v2_split_prompt_by_transitions(
- "Kai'Sa walks confidently. cut to the next action",
- placement="end",
- )
- assert len(segments) == 2
- assert "cut to" in segments[0]
- assert "Kai'Sa walks" in segments[0]
- assert "the next action" in segments[1]
- assert "cut to" not in segments[1]
-
-
-def test_split_custom_transition_placement_override(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- # custom transition with explicit "end" placement overrides global "start"
- save_transition_item({"name": "CUT", "trigger": "CUT", "placement": "end"})
-
- refiner = FunPackVideoRefinerV2()
- segments = refiner._v2_split_prompt_by_transitions(
- "Kai'Sa walks. CUT she runs.",
- placement="start", # global is start, but custom says end
- )
- assert len(segments) == 2
- assert "CUT" in segments[0]
- assert "she runs" in segments[1]
- assert "CUT" not in segments[1]
-
-
-def test_custom_transition_trigger_ending_with_punctuation(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_transition_item({"name": "Scene cut", "trigger": "Scene cut."})
-
- refiner = FunPackVideoRefinerV2()
- segments = refiner._v2_split_prompt_by_transitions(
- "Kai'Sa walks confidently. Scene cut. She looks into camera."
- )
- assert len(segments) == 2
- assert "Kai'Sa walks" in segments[0]
- assert "She looks" in segments[1]
-
-
-def test_split_transition_placement_silent(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- refiner = FunPackVideoRefinerV2()
-
- segments = refiner._v2_split_prompt_by_transitions(
- "Kai'Sa walks confidently. cut to she looks into camera.",
- placement="silent",
- )
- assert len(segments) == 2
- assert "cut to" not in segments[0]
- assert "cut to" not in segments[1]
- assert "Kai'Sa walks" in segments[0]
- assert "she looks into camera" in segments[1]
-
-
-def test_custom_transition_silent_overrides_global_start(monkeypatch, tmp_path):
- use_tmp_scene_store(monkeypatch, tmp_path)
- save_transition_item({"name": "Scene cut", "trigger": "Scene cut.", "placement": "silent"})
-
- refiner = FunPackVideoRefinerV2()
- segments = refiner._v2_split_prompt_by_transitions(
- "Kai'Sa walks. Scene cut. She looks into camera.",
- placement="start",
- )
- assert len(segments) == 2
- assert "Scene cut" not in segments[0]
- assert "Scene cut" not in segments[1]
- assert "Kai'Sa walks" in segments[0]
- assert "She looks" in segments[1]
diff --git a/tests/test_v2_category_alignment.py b/tests/test_v2_category_alignment.py
index 5dd62210..fa350631 100644
--- a/tests/test_v2_category_alignment.py
+++ b/tests/test_v2_category_alignment.py
@@ -262,84 +262,6 @@ def test_wrong_appearance_rating_is_available():
assert profile["skip_value_function"] is True
-def test_scene_builder_user_category_overrides_refiner_classification():
- refiner = FunPackVideoRefinerV2()
- scene_db = {
- "universal_memory": {
- "running pose": {
- "text": "running pose",
- "category": "appearance",
- "category_source": "user",
- "category_locked": True,
- }
- }
- }
-
- phrase = refiner._v2_classify_phrases(
- None,
- [{"text": "running pose", "tokens": refiner._v2_phrase_words("running pose")}],
- {"phrase_memory": {}},
- scene_db=scene_db,
- )[0]
-
- assert phrase["primary"] == "appearance"
- assert phrase["source"] == "scene_builder_user"
- assert phrase["scene_category_locked"] is True
-
-
-def test_scene_builder_locked_category_is_not_trained_by_refiner():
- refiner = FunPackVideoRefinerV2()
- scene_db = {
- "universal_memory": {
- "running pose": {
- "text": "running pose",
- "category": "appearance",
- "category_source": "user",
- "category_locked": True,
- }
- }
- }
- global_state = {"phrase_memory": {}}
- phrase = refiner._v2_classify_phrases(
- None,
- [{"text": "running pose", "tokens": refiner._v2_phrase_words("running pose")}],
- global_state,
- scene_db=scene_db,
- )[0]
-
- refiner._v2_update_phrase_memory(
- global_state,
- {"prompt": "running pose", "phrases": [phrase]},
- normalize_refiner_v2_rating("Perfect"),
- 1,
- refiner._v2_axis_feedback(normalize_refiner_v2_rating("Perfect"), None),
- )
-
- entry = global_state["phrase_memory"]["running pose"]
- assert entry["primary"] == "appearance"
- assert entry["category_locked"] is True
- assert entry["category_evidence_count"] == 0
- assert entry["liked_count"] == 0
-
-
-def test_scene_builder_wildcard_cleanup_runs_after_refiner_processing():
- refiner = FunPackVideoRefinerV2()
- scene_db = {
- "universal_memory": {
- "red dress": {"text": "red dress", "category": "appearance", "wildcard": True},
- "blue dress": {"text": "blue dress", "category": "appearance", "wildcard": True},
- }
- }
-
- prompt, status = refiner._v2_resolve_scene_builder_wildcards(
- "person walking, red dress, blue dress",
- scene_db,
- )
-
- assert prompt == "person walking, red dress"
- assert "removed 1 duplicate" in status
-
-
def test_lucky_composition_gates_unrelated_old_action_by_current_intent():
refiner = FunPackVideoRefinerV2()
global_state = {
@@ -1796,126 +1718,6 @@ def test_split_by_transitions_disabled_returns_single_entry(tmp_path):
assert "Transition split" not in status
-def test_split_prompt_by_transitions_detects_known_phrases():
- refiner = FunPackVideoRefinerV2()
-
- segments = refiner._v2_split_prompt_by_transitions(
- "a dog running in a field, scene one jumping over a fence, cut to stopping"
- )
- assert len(segments) == 3
-
- segments_none = refiner._v2_split_prompt_by_transitions("a dog running in a field")
- assert len(segments_none) == 1
-
- standalone_then = refiner._v2_split_prompt_by_transitions("a dog running in a field, then jumping")
- assert len(standalone_then) == 1
-
- scene_labels = refiner._v2_split_prompt_by_transitions(
- "character description, scene one shows walking, scene -999999 shows fighting, scene minus infinity shows resting"
- )
- assert len(scene_labels) == 4
-
- plural_description = refiner._v2_split_prompt_by_transitions(
- "character description shown in three distinct scenes"
- )
- assert len(plural_description) == 1
-
-
-def test_transition_scene_preview_uses_full_first_segment():
- refiner = FunPackVideoRefinerV2()
-
- segs = refiner._v2_split_prompt_by_transitions(
- "In this anime video, Hiyuki is a mature woman, scene one she walks, cut to she sits"
- )
- assert len(segs) == 3
- texts = refiner._v2_transition_scene_texts(segs)
- assert len(texts) == 2
- assert texts[0].startswith("In this anime video, Hiyuki is a mature woman")
- assert texts[1].startswith("In this anime video, Hiyuki is a mature woman")
- assert "scene one she walks" in texts[0]
- assert "she sits" in texts[1]
-
-
-def test_scene_labels_after_transitions_are_scene_text_not_boundaries(tmp_path):
- refiner = FunPackVideoRefinerV2()
- state_path = tmp_path / "state.json"
- refiner._v2_state_path = lambda refinement_key: str(state_path)
- prompt = (
- "In this 3D animated video, Kai'Sa from League of Legends is shown in three distinct scenes. "
- "Scene ten shows her walking through a neon corridor, "
- "scene -999999 shows her charging her weapons, "
- "scene minus infinity shows her landing on a rooftop."
- )
-
- segments = refiner._v2_split_prompt_by_transitions(prompt)
- texts = refiner._v2_transition_scene_texts(segments)
- assert len(texts) == 3
- assert all("Kai'Sa from League of Legends" in text for text in texts)
- assert "Scene ten shows her walking" in texts[0]
- assert "scene -999999 shows her charging" in texts[1]
- assert "scene minus infinity shows her landing" in texts[2]
- assert "shown in three distinct scenes" not in texts[0].split(", ")[-1]
-
- cond, status, _, _, encoded_prompts, _, _ = refiner.refine_v2(
- prompt,
- FakeClip(),
- "Perfect",
- "transition-label-text-test",
- split_by_transitions=True,
- )
-
- assert len(cond) == 3
- assert "Transition split: 3 scenes detected" in status
- assert "Scene 3" in encoded_prompts
- assert "Scene 4" not in encoded_prompts
-
-
-def test_prompt_starting_with_scene_label_keeps_first_scene():
- refiner = FunPackVideoRefinerV2()
- prompt = "scene ten: she walks into a room, scene one: she walks out"
-
- segments = refiner._v2_split_prompt_by_transitions(prompt)
- texts = refiner._v2_transition_scene_texts(segments)
-
- assert len(texts) == 2
- assert texts[0].startswith("scene ten she walks into a room")
- assert texts[1].startswith("scene one she walks out")
-
-
-def test_new_transition_phrases_and_then_handling():
- refiner = FunPackVideoRefinerV2()
- prompt = (
- "character anchor, as the action continues she runs, "
- "as the video progresses she turns, "
- "the final sequence shows her smiling, "
- "the final shot returns to her face, "
- "in the final moments she waves, "
- "a final shot zooms out from the city, "
- "the camera then shifts to the sky, "
- "the camera then pulls back from her, "
- "the camera shifts to the window, "
- "the camera zooms in on her eyes, "
- "finally, the scene transitions into rain, "
- "the scene transitions to a rooftop, "
- "the scene shifts to a hallway, "
- "in the next segment she sits"
- )
-
- segments = refiner._v2_split_prompt_by_transitions(prompt)
- texts = refiner._v2_transition_scene_texts(segments)
-
- assert len(texts) == 14
- assert all(text.startswith("character anchor") for text in texts)
- assert "the final shot returns to her face" in texts[3]
- assert "a final shot zooms out from the city" in texts[5]
- assert "the camera then shifts to the sky" in texts[6]
- assert "the camera then pulls back from her" in texts[7]
- assert "the camera shifts to the window" in texts[8]
- assert "the camera zooms in on her eyes" in texts[9]
- assert "finally, the scene transitions into rain" in texts[10]
- assert "the scene transitions to a rooftop" in texts[11]
-
-
def test_split_by_transitions_has_no_hard_scene_cap(tmp_path):
refiner = FunPackVideoRefinerV2()
state_path = tmp_path / "state.json"
diff --git a/web/funpack_scene_builder.js b/web/funpack_scene_builder.js
deleted file mode 100644
index acd29328..00000000
--- a/web/funpack_scene_builder.js
+++ /dev/null
@@ -1,2465 +0,0 @@
-import { app } from "../../scripts/app.js";
-import { api } from "../../scripts/api.js";
-
-const NODE_NAME = "FunPackSceneBuilder";
-const NONE_SCENE = "-None-";
-const CARET_SENTINEL = "\u200b";
-const HIDDEN_WIDGETS = new Set([
- "scene_name",
- "mode",
- "scene",
- "aliases",
- "action",
- "scene_positive",
- "scene_negative",
- "refinement_key",
- "scene_payload",
-]);
-const GROUP_ORDER = ["subject", "appearance", "action", "camera", "environment", "style", "quality", "details", "negative"];
-
-let sceneData = null;
-let shortcutData = null;
-let activePanel = null;
-const trackedNodes = new Set();
-
-function widgetByName(node, name) {
- return (node.widgets || []).find((widget) => widget.name === name);
-}
-
-function hideWidget(widget) {
- if (!widget) {
- return;
- }
- if (widget.linkedWidgets) {
- for (const linked of widget.linkedWidgets) {
- hideWidget(linked);
- }
- }
- widget.__funpackHidden = true;
- widget.hidden = true;
- widget.options = widget.options || {};
- widget.options.hidden = true;
- widget.computeSize = () => [0, -4];
- widget.computedHeight = 0;
- widget.type = "hidden";
- for (const key of ["element", "inputEl", "textElement", "parentEl"]) {
- const element = widget[key];
- if (element?.style) {
- element.style.display = "none";
- element.style.visibility = "hidden";
- element.style.pointerEvents = "none";
- }
- if (element) {
- element.hidden = true;
- }
- }
-}
-
-function getWidgetValue(node, name, fallback = "") {
- const widget = widgetByName(node, name);
- return widget ? widget.value : fallback;
-}
-
-function setWidgetValue(node, name, value) {
- const widget = widgetByName(node, name);
- if (widget) {
- widget.value = value;
- }
-}
-
-function setDirty(node) {
- node.setDirtyCanvas?.(true, true);
- app.graph?.setDirtyCanvas?.(true, true);
-}
-
-function normalizeRefinementKey(value) {
- const clean = String(value || "").trim();
- return clean && clean !== NONE_SCENE ? clean : "";
-}
-
-function fitString(ctx, text, maxWidth) {
- text = String(text ?? "");
- if (ctx.measureText(text).width <= maxWidth) {
- return text;
- }
- const ellipsis = "...";
- let low = 0;
- let high = text.length;
- while (low < high) {
- const mid = Math.ceil((low + high) / 2);
- if (ctx.measureText(text.slice(0, mid) + ellipsis).width <= maxWidth) {
- low = mid;
- } else {
- high = mid - 1;
- }
- }
- return text.slice(0, low) + ellipsis;
-}
-
-function roundRect(ctx, x, y, width, height, radius = 5) {
- ctx.beginPath();
- ctx.roundRect(x, y, width, height, [radius]);
-}
-
-function button(ctx, widget, key, label, x, y, width, height) {
- widget.hitAreas[key] = [x, y, width, height];
- roundRect(ctx, x, y, width, height, 5);
- ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR;
- ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR;
- ctx.fill();
- ctx.stroke();
- ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- ctx.fillText(fitString(ctx, label, width - 8), x + width / 2, y + height / 2);
-}
-
-function currentRefinementKey(node) {
- const linked = linkedRefinementKey(node);
- if (linked) {
- return linked;
- }
- return normalizeRefinementKey(getWidgetValue(node, "refinement_key", ""));
-}
-
-function linkedInputNode(node, inputName) {
- const input = (node.inputs || []).find((item) => item.name === inputName);
- const linkId = Array.isArray(input?.link) ? input.link[0] : input?.link;
- if (linkId == null) {
- return null;
- }
- const link = app.graph?.links?.[linkId];
- return link ? app.graph?.getNodeById?.(link.origin_id) : null;
-}
-
-function linkedRefinementKey(node) {
- const source = linkedInputNode(node, "refinement_key_input");
- if (!source) {
- return "";
- }
- const selected = normalizeRefinementKey(getWidgetValue(source, "refinement_key", ""));
- const typed = normalizeRefinementKey(getWidgetValue(source, "key_name", ""));
- return selected || typed;
-}
-
-function linkedTextValue(node, inputName) {
- const source = linkedInputNode(node, inputName);
- if (!source) {
- return "";
- }
- for (const name of ["text", "prompt", "positive_prompt", "negative_prompt", "string", "value"]) {
- const value = getWidgetValue(source, name, "");
- if (String(value || "").trim()) {
- return String(value);
- }
- }
- return "";
-}
-
-function sceneNames() {
- const names = sceneData?.scenes;
- return Array.isArray(names) && names.length ? names : [NONE_SCENE];
-}
-
-function sceneByName(name) {
- return sceneData?.data?.scenes?.[name] || null;
-}
-
-function savedSceneNames() {
- return sceneNames().filter((item) => item !== NONE_SCENE);
-}
-
-function activeSceneName(node) {
- const typed = String(getWidgetValue(node, "scene_name", "") || "").trim();
- const selected = String(getWidgetValue(node, "scene", "") || "").trim();
- if (typed) {
- return typed;
- }
- return selected && selected !== NONE_SCENE ? selected : "";
-}
-
-function needsSceneCreation(node) {
- return !activeSceneName(node) && !savedSceneNames().length;
-}
-
-function memoryItems() {
- return Array.isArray(sceneData?.memory) ? sceneData.memory : [];
-}
-
-function shortcutItems() {
- return Array.isArray(shortcutData?.shortcuts) ? shortcutData.shortcuts : [];
-}
-
-function normalizeMemoryMap(items) {
- const output = {};
- for (const item of items || []) {
- const text = String(item.text || item.key || "").trim();
- if (!text) {
- continue;
- }
- const key = String(item.key || text).toLowerCase().replace(/[^\w'’]+/g, " ").replace(/\s+/g, " ").trim() || text.toLowerCase();
- output[key] = {
- text,
- key,
- source: item.source === "negative" || item.category === "negative" ? "negative" : "positive",
- category: GROUP_ORDER.includes(item.category) ? item.category : "details",
- tokens: Array.isArray(item.tokens) ? item.tokens : [],
- count: Number.isFinite(Number(item.count)) ? Number(item.count) : 0,
- created_at: item.created_at || new Date().toISOString(),
- updated_at: new Date().toISOString(),
- wildcard: Boolean(item.wildcard || String(item.wildcard_group || "").trim()),
- };
- }
- return output;
-}
-
-async function fetchScenes(refinementKey = "") {
- try {
- const params = new URLSearchParams({ cache_bust: String(Date.now()) });
- if (refinementKey) {
- params.set("key", refinementKey);
- }
- const response = await api.fetchApi(`/funpack/scenes?${params.toString()}`, { cache: "no-store" });
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- sceneData = await response.json();
- } catch (error) {
- console.warn("FunPack: failed to refresh scenes", error);
- }
- return sceneData;
-}
-
-async function fetchShortcuts() {
- try {
- const params = new URLSearchParams({ cache_bust: String(Date.now()) });
- const response = await api.fetchApi(`/funpack/shortcuts?${params.toString()}`, { cache: "no-store" });
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}`);
- }
- shortcutData = await response.json();
- } catch (error) {
- console.warn("FunPack: failed to refresh shortcuts", error);
- }
- return shortcutData;
-}
-
-function updateSceneCombo(node) {
- const widget = widgetByName(node, "scene");
- if (!widget) {
- return;
- }
- const values = sceneNames();
- const current = widget.value;
- widget.options.values = current && !values.includes(current) ? [...values, current] : values;
- if (!widget.value || !widget.options.values.includes(widget.value)) {
- widget.value = widget.options.values[0] || NONE_SCENE;
- }
-}
-
-async function refreshNode(node) {
- await Promise.all([fetchScenes(currentRefinementKey(node)), fetchShortcuts()]);
- updateSceneCombo(node);
- setDirty(node);
-}
-
-async function refreshTracked() {
- for (const node of [...trackedNodes]) {
- if (!node?.graph) {
- trackedNodes.delete(node);
- continue;
- }
- await refreshNode(node);
- }
-}
-
-function scheduleRefresh() {
- window.setTimeout(() => void refreshTracked(), 100);
-}
-
-function loadSceneIntoNode(node, name) {
- const scene = sceneByName(name);
- if (!scene || name === NONE_SCENE) {
- return;
- }
- setWidgetValue(node, "scene", name);
- setWidgetValue(node, "scene_name", scene.name || name);
- setWidgetValue(node, "aliases", Array.isArray(scene.aliases) ? scene.aliases.join(", ") : "");
- setWidgetValue(node, "refinement_key", scene.refinement_key || "");
- setWidgetValue(node, "scene_positive", Object.prototype.hasOwnProperty.call(scene, "positive_text") ? (scene.positive_text || "") : (scene.positive_phrases || []).join(", "));
- setWidgetValue(node, "scene_negative", Object.prototype.hasOwnProperty.call(scene, "negative_text") ? (scene.negative_text || "") : (scene.negative_phrases || []).join(", "));
- setDirty(node);
-}
-
-async function saveSceneFromNode(node) {
- const name = String(getWidgetValue(node, "scene_name", "") || "").trim();
- if (!name) {
- throw new Error("Scene name is required.");
- }
- const params = new URLSearchParams();
- const key = currentRefinementKey(node);
- if (key) {
- params.set("key", key);
- }
- const response = await api.fetchApi(`/funpack/scenes/scene${params.toString() ? `?${params.toString()}` : ""}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- action: "save",
- name,
- aliases: getWidgetValue(node, "aliases", ""),
- mode: getWidgetValue(node, "mode", "Manual"),
- positive_text: getWidgetValue(node, "scene_positive", ""),
- negative_text: getWidgetValue(node, "scene_negative", ""),
- }),
- });
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.error || `Save failed with HTTP ${response.status}`);
- }
- await refreshNode(node);
- setWidgetValue(node, "scene", name);
-}
-
-async function deleteSceneFromNode(node) {
- const name = String(getWidgetValue(node, "scene_name", "") || "").trim();
- if (!name) {
- throw new Error("Scene name is required.");
- }
- const params = new URLSearchParams();
- const key = currentRefinementKey(node);
- if (key) {
- params.set("key", key);
- }
- const response = await api.fetchApi(`/funpack/scenes/scene${params.toString() ? `?${params.toString()}` : ""}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action: "delete", name }),
- });
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.error || `Delete failed with HTTP ${response.status}`);
- }
- setWidgetValue(node, "scene", NONE_SCENE);
- setWidgetValue(node, "scene_name", "");
- setWidgetValue(node, "scene_positive", "");
- setWidgetValue(node, "scene_negative", "");
- await refreshNode(node);
-}
-
-async function saveDatabase(node, items) {
- const params = new URLSearchParams();
- const key = currentRefinementKey(node);
- if (key) {
- params.set("key", key);
- }
- const response = await api.fetchApi(`/funpack/scenes/database${params.toString() ? `?${params.toString()}` : ""}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ universal_memory: normalizeMemoryMap(items) }),
- });
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.error || `Database save failed with HTTP ${response.status}`);
- }
- await refreshNode(node);
-}
-
-async function saveShortcut(item) {
- const response = await api.fetchApi("/funpack/shortcuts/shortcut", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action: "save", ...item }),
- });
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.error || `Shortcut save failed with HTTP ${response.status}`);
- }
- shortcutData = await response.json();
- return shortcutData;
-}
-
-async function deleteShortcut(name) {
- const response = await api.fetchApi("/funpack/shortcuts/shortcut", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action: "delete", name }),
- });
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.error || `Shortcut delete failed with HTTP ${response.status}`);
- }
- shortcutData = await response.json();
- return shortcutData;
-}
-
-async function exportShortcuts() {
- const params = new URLSearchParams({ cache_bust: String(Date.now()) });
- const response = await api.fetchApi(`/funpack/shortcuts/export?${params.toString()}`, { cache: "no-store" });
- if (!response.ok) {
- throw new Error(`Shortcut export failed with HTTP ${response.status}`);
- }
- const blob = await response.blob();
- const url = URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = "funpack_shortcuts.json";
- document.body.appendChild(link);
- link.click();
- link.remove();
- URL.revokeObjectURL(url);
-}
-
-function importShortcuts(onDone) {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".json,application/json";
- input.onchange = async () => {
- const file = input.files?.[0];
- if (!file) {
- return;
- }
- try {
- const data = JSON.parse(await file.text());
- const response = await api.fetchApi("/funpack/shortcuts/import", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(data),
- });
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.error || `Shortcut import failed with HTTP ${response.status}`);
- }
- shortcutData = await response.json();
- onDone?.();
- } catch (error) {
- showPanelError(error);
- }
- };
- input.click();
-}
-
-async function exportScenes(node) {
- const params = new URLSearchParams({ cache_bust: String(Date.now()) });
- const key = currentRefinementKey(node);
- if (key) {
- params.set("key", key);
- }
- const response = await api.fetchApi(`/funpack/scenes/export?${params.toString()}`, { cache: "no-store" });
- if (!response.ok) {
- throw new Error(`Export failed with HTTP ${response.status}`);
- }
- const blob = await response.blob();
- const url = URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.download = "funpack_scenes.json";
- document.body.appendChild(link);
- link.click();
- link.remove();
- URL.revokeObjectURL(url);
-}
-
-function importScenes(node) {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".json,application/json";
- input.onchange = async () => {
- const file = input.files?.[0];
- if (!file) {
- return;
- }
- try {
- const data = JSON.parse(await file.text());
- const params = new URLSearchParams();
- const key = currentRefinementKey(node);
- if (key) {
- params.set("key", key);
- }
- const response = await api.fetchApi(`/funpack/scenes/import${params.toString() ? `?${params.toString()}` : ""}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(data),
- });
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.error || `Import failed with HTTP ${response.status}`);
- }
- await refreshNode(node);
- renderPanel(activePanel, node, "menu");
- } catch (error) {
- showPanelError(error);
- }
- };
- input.click();
-}
-
-function closePanel() {
- if (activePanel?.root) {
- activePanel.root.remove();
- }
- activePanel = null;
-}
-
-function showPanelError(error) {
- const target = activePanel?.root?.querySelector("[data-role='error']");
- if (target) {
- target.textContent = error.message || String(error);
- } else {
- console.warn("FunPack Scene Builder:", error);
- }
-}
-
-function panelButton(label, className = "") {
- const element = document.createElement("button");
- element.type = "button";
- element.textContent = label;
- element.className = `funpack-scene-button ${className}`.trim();
- return element;
-}
-
-function panelTextInput(value, placeholder) {
- const element = document.createElement("input");
- element.type = "text";
- element.value = String(value || "");
- element.placeholder = placeholder;
- return element;
-}
-
-function panelSelect(values, selected) {
- const element = document.createElement("select");
- for (const value of values) {
- const option = document.createElement("option");
- option.value = value;
- option.textContent = value;
- option.selected = value === selected;
- element.append(option);
- }
- 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 editableTextLabel(value, onCommit, options = {}) {
- const wrapper = document.createElement("div");
- wrapper.className = "funpack-scene-editable";
- const label = document.createElement("button");
- label.type = "button";
- let currentValue = String(value || "").trim();
- const updateLabel = () => {
- label.textContent = currentValue || "Unnamed";
- label.title = currentValue ? `${currentValue}\n\nDouble-click to edit.` : "Double-click to edit.";
- label.setAttribute("aria-label", currentValue ? `${currentValue}. Double-click to edit.` : "Double-click to edit.");
- };
- updateLabel();
- wrapper.append(label);
-
- const beginEdit = () => {
- const editor = document.createElement("div");
- editor.className = "funpack-scene-inline-editor";
- const multiline = Boolean(options.multiline);
- if (multiline) {
- editor.classList.add("multiline");
- wrapper.classList.add("editing");
- }
- const input = document.createElement(multiline ? "textarea" : "input");
- const original = currentValue || label.textContent || "";
- if (!multiline) {
- input.type = "text";
- } else {
- input.rows = 4;
- input.className = "funpack-scene-edit-textarea";
- }
- input.value = original;
- const ok = panelButton("OK", "compact primary");
- const cancel = panelButton("Cancel", "compact");
- const actions = document.createElement("div");
- actions.className = "funpack-scene-inline-actions";
- let committed = false;
- const restore = () => {
- wrapper.classList.remove("editing");
- editor.replaceWith(label);
- };
- const commit = () => {
- if (committed) {
- return;
- }
- committed = true;
- const next = input.value.trim();
- currentValue = next || original;
- onCommit(currentValue);
- updateLabel();
- restore();
- };
- const cancelEdit = () => {
- if (committed) {
- return;
- }
- committed = true;
- restore();
- };
- input.addEventListener("keydown", (event) => {
- if (!multiline && event.key === "Enter") {
- event.preventDefault();
- commit();
- } else if (multiline && event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
- event.preventDefault();
- commit();
- } else if (event.key === "Escape") {
- event.preventDefault();
- cancelEdit();
- }
- });
- ok.addEventListener("click", commit);
- cancel.addEventListener("click", cancelEdit);
- actions.append(ok, cancel);
- editor.append(input, actions);
- label.replaceWith(editor);
- input.focus();
- input.select();
- };
-
- label.addEventListener("dblclick", beginEdit);
- label.addEventListener("keydown", (event) => {
- if (event.key === "Enter" || event.key === "F2") {
- event.preventDefault();
- beginEdit();
- }
- });
- return wrapper;
-}
-
-function phraseButton(text, onInsert) {
- const element = document.createElement("button");
- element.type = "button";
- element.className = "funpack-scene-chip";
- element.textContent = text;
- element.title = text;
- element.draggable = true;
- element.addEventListener("click", () => onInsert(text));
- element.addEventListener("dragstart", (event) => {
- event.dataTransfer?.setData("text/plain", text);
- });
- return element;
-}
-
-function normalizePromptSpacing(value) {
- return String(value || "")
- .replace(/\s+([,.])/g, "$1")
- .replace(/([,.])([^\s,.])/g, "$1 $2")
- .replace(/[ \t\r\f\v]+/g, " ")
- .trim();
-}
-
-function normalizePromptEditorValue(value) {
- return String(value || "")
- .replaceAll(CARET_SENTINEL, "")
- .replace(/\u00a0/g, " ")
- .replace(/[ \t\r\f\v]+([,.])/g, "$1");
-}
-
-function promptLookupKey(value) {
- return String(value || "")
- .toLowerCase()
- .replace(/[^\w'’]+/g, " ")
- .replace(/\s+/g, " ")
- .trim();
-}
-
-function promptContainsPhrase(prompt, phrase) {
- const promptKey = promptLookupKey(prompt);
- const phraseKey = promptLookupKey(phrase);
- if (!promptKey || !phraseKey) {
- return false;
- }
- return ` ${promptKey} `.includes(` ${phraseKey} `);
-}
-
-function promptBoundary(value, index) {
- if (index < 0 || index >= value.length) {
- return true;
- }
- return !/[\w'’]/.test(value[index]);
-}
-
-function promptPhraseRanges(prompt, phrases) {
- const source = String(prompt || "");
- const ranges = [];
- const occupied = new Array(source.length).fill(false);
- const candidates = [...new Set((phrases || []).map((phrase) => normalizePromptSpacing(phrase)).filter(Boolean))]
- .sort((left, right) => right.length - left.length || left.localeCompare(right));
-
- for (const phrase of candidates) {
- const pattern = phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
- const regex = new RegExp(pattern, "gi");
- let match = regex.exec(source);
- while (match) {
- const index = match.index;
- const end = index + match[0].length;
- const overlaps = occupied.slice(index, end).some(Boolean);
- if (!overlaps && promptBoundary(source, index - 1) && promptBoundary(source, end)) {
- ranges.push({ start: index, end, phrase, label: source.slice(index, end) });
- for (let cursor = index; cursor < end; cursor += 1) {
- occupied[cursor] = true;
- }
- }
- match = regex.exec(source);
- }
- }
-
- return ranges.sort((left, right) => left.start - right.start || right.end - left.end);
-}
-
-function promptMemoryPhrases(isNegative = false) {
- return memoryItems()
- .filter((item) => isNegative ? item.source === "negative" || item.category === "negative" : item.source !== "negative" && item.category !== "negative")
- .map((item) => item.text || item.key || "")
- .filter(Boolean);
-}
-
-function richPromptText(root) {
- let output = "";
- const append = (node) => {
- if (node.nodeType === Node.TEXT_NODE) {
- output += node.nodeValue || "";
- return;
- }
- if (node.nodeType !== Node.ELEMENT_NODE) {
- return;
- }
- if (node.matches?.(".funpack-scene-inline-token")) {
- output += node.textContent || "";
- return;
- }
- if (node.tagName === "BR") {
- output += "\n";
- return;
- }
- for (const child of node.childNodes) {
- append(child);
- }
- };
- for (const child of root.childNodes) {
- append(child);
- }
- return normalizePromptEditorValue(output);
-}
-
-function richNodeTextLength(node) {
- if (!node) {
- return 0;
- }
- if (node.nodeType === Node.TEXT_NODE) {
- return normalizePromptEditorValue(node.nodeValue || "").length;
- }
- if (node.nodeType !== Node.ELEMENT_NODE) {
- return 0;
- }
- if (node.matches?.(".funpack-scene-inline-token")) {
- return (node.textContent || "").length;
- }
- if (node.tagName === "BR") {
- return 1;
- }
- let length = 0;
- for (const child of node.childNodes) {
- length += richNodeTextLength(child);
- }
- return length;
-}
-
-function richRangeTextOffset(root, range) {
- if (!root || !range || !root.contains(range.startContainer)) {
- return null;
- }
- let offset = 0;
- let found = false;
- const visit = (node) => {
- if (found) {
- return;
- }
- if (node === range.startContainer) {
- if (node.nodeType === Node.TEXT_NODE) {
- offset += normalizePromptEditorValue((node.nodeValue || "").slice(0, range.startOffset)).length;
- } else {
- const children = [...node.childNodes];
- for (let index = 0; index < Math.min(range.startOffset, children.length); index += 1) {
- offset += richNodeTextLength(children[index]);
- }
- }
- found = true;
- return;
- }
- if (node.nodeType === Node.TEXT_NODE || node.matches?.(".funpack-scene-inline-token") || node.tagName === "BR") {
- offset += richNodeTextLength(node);
- return;
- }
- for (const child of node.childNodes || []) {
- visit(child);
- if (found) {
- return;
- }
- }
- };
- visit(root);
- return found ? offset : null;
-}
-
-function isCaretSentinelNode(node) {
- return node?.nodeType === Node.TEXT_NODE && node.nodeValue === CARET_SENTINEL;
-}
-
-function lastPromptContentChild(root) {
- const children = [...(root?.childNodes || [])];
- for (let index = children.length - 1; index >= 0; index -= 1) {
- const child = children[index];
- if (isCaretSentinelNode(child)) {
- continue;
- }
- if (child.nodeType === Node.TEXT_NODE && !normalizePromptEditorValue(child.nodeValue || "").length) {
- continue;
- }
- return child;
- }
- return null;
-}
-
-function ensureTrailingCaretTarget(root) {
- for (const child of [...(root?.childNodes || [])]) {
- if (isCaretSentinelNode(child)) {
- child.remove();
- }
- }
- if (lastPromptContentChild(root)?.matches?.(".funpack-scene-inline-token")) {
- root.append(document.createTextNode(CARET_SENTINEL));
- }
-}
-
-function setCaretToEnd(element) {
- element.focus();
- const range = document.createRange();
- range.selectNodeContents(element);
- range.collapse(false);
- const selection = window.getSelection();
- selection?.removeAllRanges();
- selection?.addRange(range);
-}
-
-function setCaretAtElementEdge(node, after = false) {
- const range = document.createRange();
- if (after) {
- range.setStartAfter(node);
- } else {
- range.setStartBefore(node);
- }
- range.collapse(true);
- const selection = window.getSelection();
- selection?.removeAllRanges();
- selection?.addRange(range);
-}
-
-function setCaretByTextOffset(root, targetOffset) {
- const target = Math.max(0, Number(targetOffset) || 0);
- let offset = 0;
- let placed = false;
- const place = (node) => {
- if (placed) {
- return;
- }
- if (node.nodeType === Node.TEXT_NODE) {
- const textLength = (node.nodeValue || "").length;
- if (target <= offset + textLength) {
- setCaretInTextNode(node, target - offset);
- placed = true;
- } else {
- offset += textLength;
- }
- return;
- }
- if (node.nodeType === Node.ELEMENT_NODE && node.matches?.(".funpack-scene-inline-token")) {
- const tokenLength = (node.textContent || "").length;
- if (target <= offset + tokenLength) {
- setCaretAtElementEdge(node, target > offset);
- placed = true;
- } else {
- offset += tokenLength;
- }
- return;
- }
- if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "BR") {
- if (target <= offset + 1) {
- setCaretAtElementEdge(node, true);
- placed = true;
- } else {
- offset += 1;
- }
- return;
- }
- for (const child of node.childNodes || []) {
- place(child);
- if (placed) {
- return;
- }
- }
- };
- root.focus();
- place(root);
- if (!placed) {
- setCaretToEnd(root);
- }
-}
-
-function setCaretInTextNode(node, offset) {
- const range = document.createRange();
- range.setStart(node, Math.max(0, Math.min(offset, (node.nodeValue || "").length)));
- range.collapse(true);
- const selection = window.getSelection();
- selection?.removeAllRanges();
- selection?.addRange(range);
-}
-
-function setCaretAfterInlineToken(token) {
- if (!token) {
- return;
- }
- const range = document.createRange();
- range.setStartAfter(token);
- range.collapse(true);
- const selection = window.getSelection();
- selection?.removeAllRanges();
- selection?.addRange(range);
-}
-
-function placeCaretAfterLastTokenFromMouse(editor, event) {
- if (event.target !== editor) {
- return false;
- }
- const last = lastPromptContentChild(editor);
- if (!last?.matches?.(".funpack-scene-inline-token")) {
- return false;
- }
- const rect = last.getBoundingClientRect();
- const sameLineAfterToken = event.clientY >= rect.top - 4 && event.clientY <= rect.bottom + 8 && event.clientX >= rect.right - 4;
- const belowToken = event.clientY > rect.bottom + 8;
- if (!sameLineAfterToken && !belowToken) {
- return false;
- }
- editor.focus();
- setCaretAfterInlineToken(last);
- return true;
-}
-
-function insertIntoRichPrompt(editor, text, fallbackRange = null) {
- editor.focus();
- const selection = window.getSelection();
- const activeRange = selection?.rangeCount && editor.contains(selection.anchorNode)
- ? selection.getRangeAt(0)
- : fallbackRange;
- if (activeRange && editor.contains(activeRange.commonAncestorContainer)) {
- const promptBeforeEdit = richPromptText(editor);
- const beforeOffset = richRangeTextOffset(editor, activeRange) ?? richNodeTextLength(editor);
- const endRange = activeRange.cloneRange();
- endRange.collapse(false);
- const afterOffset = richRangeTextOffset(editor, endRange) ?? beforeOffset;
- const beforeText = promptBeforeEdit.slice(0, beforeOffset);
- const afterText = promptBeforeEdit.slice(afterOffset);
- const prefix = beforeText.trim() && !/[,\s.;\n]$/.test(beforeText) ? ", " : "";
- const suffix = afterText.trim() && !/^[,\s.;\n]/.test(afterText) ? ", " : "";
- activeRange.deleteContents();
- const insertion = `${prefix}${text}${suffix}`;
- const node = document.createTextNode(insertion);
- activeRange.insertNode(node);
- activeRange.setStartAfter(node);
- activeRange.collapse(true);
- selection?.removeAllRanges();
- selection?.addRange(activeRange);
- return beforeOffset + insertion.length;
- }
- const current = richPromptText(editor);
- const insertion = `${current.trim() && !/[,\s.;\n]$/.test(current) ? ", " : ""}${text}`;
- editor.append(document.createTextNode(insertion));
- setCaretToEnd(editor);
- return richNodeTextLength(editor);
-}
-
-function inlineTokenFromNode(node, root) {
- if (!node || !root) {
- return null;
- }
- const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
- const token = element?.closest?.(".funpack-scene-inline-token");
- return token && root.contains(token) ? token : null;
-}
-
-function nearestEditableChild(node, root) {
- if (!node || node === root) {
- return null;
- }
- let child = node;
- while (child?.parentNode && child.parentNode !== root) {
- child = child.parentNode;
- }
- return child?.parentNode === root ? child : null;
-}
-
-function adjacentInlineToken(editor, direction) {
- const selection = window.getSelection();
- if (!selection || !selection.rangeCount) {
- return null;
- }
- const range = selection.getRangeAt(0);
- if (!editor.contains(range.commonAncestorContainer)) {
- return null;
- }
- if (!range.collapsed) {
- for (const token of editor.querySelectorAll(".funpack-scene-inline-token")) {
- if (range.intersectsNode(token)) {
- return token;
- }
- }
- return null;
- }
-
- const node = range.startContainer;
- const offset = range.startOffset;
- const token = inlineTokenFromNode(node, editor);
- if (token) {
- return token;
- }
-
- if (node.nodeType === Node.TEXT_NODE) {
- const atEdge = direction === "backward" ? offset === 0 : offset === (node.nodeValue || "").length;
- if (!atEdge) {
- return null;
- }
- const sibling = direction === "backward" ? node.previousSibling : node.nextSibling;
- return inlineTokenFromNode(sibling, editor);
- }
-
- if (node.nodeType === Node.ELEMENT_NODE) {
- const children = node.childNodes || [];
- const child = node === editor ? children[direction === "backward" ? offset - 1 : offset] : nearestEditableChild(node, editor);
- if (child) {
- const sibling = node === editor ? child : (direction === "backward" ? child.previousSibling : child.nextSibling);
- return inlineTokenFromNode(sibling || child, editor);
- }
- }
-
- return null;
-}
-
-function editInlineTokenAsText(editor, token, direction) {
- if (!token) {
- return false;
- }
- const text = token.textContent || "";
- const next = direction === "backward" ? text.slice(0, -1) : text.slice(1);
- const caretOffset = direction === "backward" ? next.length : 0;
- const replacement = document.createTextNode(next);
- token.replaceWith(replacement);
- editor.focus();
- setCaretInTextNode(replacement, caretOffset);
- return true;
-}
-
-function promptWords(value) {
- const key = promptLookupKey(value);
- return key ? key.split(" ").filter((word) => word.length >= 2) : [];
-}
-
-function removePhraseFromPrompt(prompt, phrase) {
- const phraseKey = promptLookupKey(phrase);
- if (!phraseKey) {
- return prompt;
- }
- const parts = String(prompt || "").split(/([,;.\n]+)/);
- let changed = false;
- for (let index = 0; index < parts.length; index += 2) {
- const segment = parts[index];
- const segmentKey = promptLookupKey(segment);
- if (!segmentKey) {
- continue;
- }
- if (segmentKey === phraseKey) {
- parts[index] = "";
- changed = true;
- continue;
- }
- const padded = ` ${segmentKey} `;
- if (padded.includes(` ${phraseKey} `)) {
- const words = phraseKey.split(" ").map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
- const pattern = new RegExp(`(^|[^\\w'’])(${words.join("[^\\w'’]+")})(?=$|[^\\w'’])`, "i");
- parts[index] = segment.replace(pattern, "$1").replace(/\s+/g, " ").trim();
- changed = true;
- }
- }
- if (!changed) {
- return prompt;
- }
- const output = parts.join("");
- return normalizePromptSpacing(output.replace(/\s*([,;.])\s*([,;.]\s*)+/g, "$1 "));
-}
-
-function preconstructPrompt(currentPrompt, intentText, isNegative = false) {
- if (isNegative) {
- return { prompt: currentPrompt, added: [] };
- }
- const intentWords = new Set(promptWords(intentText));
- if (!intentWords.size) {
- return { prompt: currentPrompt, added: [] };
- }
- const prompt = normalizePromptSpacing(currentPrompt);
- const intentKey = promptLookupKey(intentText);
- const candidates = [];
- for (const item of memoryItems()) {
- if (item.source === "negative" || item.category === "negative") {
- continue;
- }
- const text = normalizePromptSpacing(item.text || item.key || "");
- const key = promptLookupKey(text);
- if (!key || promptContainsPhrase(prompt, text)) {
- continue;
- }
- const words = promptWords(text);
- const overlap = words.filter((word) => intentWords.has(word)).length;
- const exact = intentKey && ` ${intentKey} `.includes(` ${key} `);
- if (!exact && !overlap) {
- continue;
- }
- const count = Number.isFinite(Number(item.count)) ? Number(item.count) : 0;
- const score = (exact ? 4 : 0) + overlap / Math.max(1, words.length) + Math.min(1.5, count * 0.04) + Math.min(0.5, words.length * 0.04);
- candidates.push({ text, score, words: words.length, count });
- }
- candidates.sort((left, right) => right.score - left.score || right.words - left.words || right.count - left.count || left.text.localeCompare(right.text));
- const added = [];
- const seen = new Set(promptWords(prompt));
- for (const candidate of candidates) {
- const key = promptLookupKey(candidate.text);
- if (!key || added.some((text) => promptLookupKey(text) === key)) {
- continue;
- }
- added.push(candidate.text);
- for (const word of promptWords(candidate.text)) {
- seen.add(word);
- }
- if (added.length >= 16) {
- break;
- }
- }
- if (!added.length) {
- return { prompt, added };
- }
- return {
- prompt: normalizePromptSpacing(`${prompt}${prompt ? ", " : ""}${added.join(", ")}`),
- added,
- };
-}
-
-function shell(root, title, view) {
- root.replaceChildren();
- const header = document.createElement("div");
- header.className = "funpack-scene-panel-header";
- const h = document.createElement("div");
- h.textContent = title;
- const close = panelButton("Close");
- close.addEventListener("click", closePanel);
- header.append(h, close);
-
- const error = document.createElement("div");
- error.dataset.role = "error";
- error.className = "funpack-scene-error";
-
- const body = document.createElement("div");
- body.className = `funpack-scene-body funpack-scene-${view}`;
- root.append(header, error, body);
- return body;
-}
-
-function renderPanel(panel, node, view, options = {}) {
- if (!panel?.root) {
- return;
- }
- if (options.cancelTarget) {
- panel.cancelTarget = options.cancelTarget;
- }
- panel.currentView = view;
- if (view === "positive" || view === "negative") {
- renderPromptEditor(panel, node, view);
- } else if (view === "database") {
- renderDatabaseEditor(panel, node);
- } else if (view === "shortcuts") {
- renderShortcutEditor(panel, node);
- } else if (view === "create") {
- renderCreateScene(panel, node, options.nextView || "menu");
- } else {
- renderMenu(panel, node);
- }
-}
-
-function navigatePanel(panel, node, view, options = {}) {
- if (["menu", "positive", "negative", "database"].includes(view) && needsSceneCreation(node)) {
- renderPanel(panel, node, "create", { ...options, nextView: view });
- return;
- }
- renderPanel(panel, node, view, options);
-}
-
-function cancelEditor(panel, node) {
- if (panel?.cancelTarget === "close") {
- closePanel();
- return;
- }
- renderPanel(panel, node, "menu");
-}
-
-function renderCreateScene(panel, node, nextView = "menu") {
- const body = shell(panel.root, "Create scene", "create");
- const message = document.createElement("div");
- message.className = "funpack-scene-muted";
- message.textContent = "Create a scene before editing prompts or database choices.";
- body.append(message);
-
- const row = document.createElement("label");
- row.className = "funpack-scene-create-row";
- const label = document.createElement("span");
- label.textContent = "Scene name";
- const input = panelTextInput(getWidgetValue(node, "scene_name", ""), "Scene name");
- row.append(label, input);
- body.append(row);
-
- const footer = document.createElement("div");
- footer.className = "funpack-scene-footer";
- const cancel = panelButton("Cancel");
- cancel.addEventListener("click", closePanel);
- const create = panelButton("Create scene", "primary");
- create.addEventListener("click", () => {
- const name = input.value.trim();
- if (!name) {
- showPanelError(new Error("Scene name is required."));
- input.focus();
- return;
- }
- setWidgetValue(node, "scene_name", name);
- setWidgetValue(node, "scene", NONE_SCENE);
- setWidgetValue(node, "mode", getWidgetValue(node, "mode", "Manual") || "Manual");
- setDirty(node);
- renderPanel(panel, node, nextView, {
- cancelTarget: nextView === "menu" ? "menu" : panel.cancelTarget,
- });
- });
- input.addEventListener("keydown", (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- create.click();
- }
- });
- footer.append(cancel, create);
- body.append(footer);
- input.focus();
-}
-
-function renderMenu(panel, node) {
- const body = shell(panel.root, "FunPack Scene Builder", "menu");
-
- const controls = document.createElement("div");
- controls.className = "funpack-scene-controls";
- const sceneSelect = panelSelect(sceneNames(), String(getWidgetValue(node, "scene", NONE_SCENE) || NONE_SCENE));
- sceneSelect.addEventListener("change", () => {
- const name = sceneSelect.value;
- if (name === NONE_SCENE) {
- setWidgetValue(node, "scene", NONE_SCENE);
- setWidgetValue(node, "scene_name", "");
- setWidgetValue(node, "aliases", "");
- setWidgetValue(node, "scene_positive", "");
- setWidgetValue(node, "scene_negative", "");
- setDirty(node);
- renderPanel(panel, node, "menu");
- return;
- }
- loadSceneIntoNode(node, name);
- renderPanel(panel, node, "menu");
- });
- const sceneName = panelTextInput(getWidgetValue(node, "scene_name", ""), "Scene name");
- sceneName.addEventListener("input", () => {
- setWidgetValue(node, "scene_name", sceneName.value);
- setDirty(node);
- });
- const mode = panelSelect(["Manual", "Auto", "Learning"], String(getWidgetValue(node, "mode", "Manual") || "Manual"));
- mode.addEventListener("change", () => {
- setWidgetValue(node, "mode", mode.value);
- setDirty(node);
- });
- const aliases = panelTextInput(getWidgetValue(node, "aliases", ""), "Aliases, comma separated");
- aliases.addEventListener("input", () => {
- setWidgetValue(node, "aliases", aliases.value);
- setDirty(node);
- });
- for (const [label, element] of [
- ["Saved scene", sceneSelect],
- ["Scene name", sceneName],
- ["Mode", mode],
- ["Aliases", aliases],
- ]) {
- const row = document.createElement("label");
- const span = document.createElement("span");
- span.textContent = label;
- row.append(span, element);
- controls.append(row);
- }
- body.append(controls);
-
- const current = document.createElement("div");
- current.className = "funpack-scene-current";
- const positive = String(getWidgetValue(node, "scene_positive", "") || "").trim();
- const negative = String(getWidgetValue(node, "scene_negative", "") || "").trim();
- for (const [label, value] of [
- ["Positive", positive || "empty"],
- ["Negative", negative || "empty"],
- ]) {
- const row = document.createElement("div");
- const b = document.createElement("b");
- b.textContent = label;
- const span = document.createElement("span");
- span.textContent = value;
- row.append(b, span);
- current.append(row);
- }
- body.append(current);
-
- const menu = document.createElement("div");
- menu.className = "funpack-scene-menu-buttons";
- for (const [label, target] of [["Positive prompt", "positive"], ["Negative prompt", "negative"], ["Database", "database"], ["Shortcuts", "shortcuts"]]) {
- const item = panelButton(label, "large");
- item.addEventListener("click", () => navigatePanel(panel, node, target, { cancelTarget: "menu" }));
- menu.append(item);
- }
- body.append(menu);
-
- const savedHeader = document.createElement("div");
- savedHeader.className = "funpack-scene-section-title";
- savedHeader.textContent = "Saved scenes";
- body.append(savedHeader);
-
- const saved = document.createElement("div");
- saved.className = "funpack-scene-saved";
- for (const name of savedSceneNames()) {
- const item = panelButton(name);
- item.title = name;
- item.addEventListener("click", () => {
- loadSceneIntoNode(node, name);
- renderPanel(panel, node, "menu");
- });
- saved.append(item);
- }
- if (!saved.children.length) {
- const empty = document.createElement("div");
- empty.className = "funpack-scene-muted";
- empty.textContent = "No saved scenes yet.";
- saved.append(empty);
- }
- body.append(saved);
-
- const footer = document.createElement("div");
- footer.className = "funpack-scene-footer";
- const refresh = panelButton("Refresh");
- refresh.addEventListener("click", async () => {
- await refreshNode(node);
- renderPanel(panel, node, "menu");
- });
- const save = panelButton("Save scene", "primary");
- save.addEventListener("click", async () => {
- try {
- await saveSceneFromNode(node);
- renderPanel(panel, node, "menu");
- } catch (error) {
- showPanelError(error);
- }
- });
- const del = panelButton("Delete scene", "danger");
- del.addEventListener("click", async () => {
- try {
- await deleteSceneFromNode(node);
- renderPanel(panel, node, "menu");
- } catch (error) {
- showPanelError(error);
- }
- });
- const imp = panelButton("Import");
- imp.addEventListener("click", () => importScenes(node));
- const exp = panelButton("Export");
- exp.addEventListener("click", async () => {
- try {
- await exportScenes(node);
- } catch (error) {
- showPanelError(error);
- }
- });
- footer.append(refresh, save, del, imp, exp);
- body.append(footer);
-}
-
-function renderPromptEditor(panel, node, kind) {
- const isNegative = kind === "negative";
- const widgetName = isNegative ? "scene_negative" : "scene_positive";
- const snapshotKey = `${kind}_snapshot`;
- if (!(snapshotKey in panel.snapshots)) {
- panel.snapshots[snapshotKey] = String(getWidgetValue(node, widgetName, "") || "");
- }
-
- const body = shell(panel.root, isNegative ? "Negative prompt" : "Positive prompt", "prompt");
- const composer = document.createElement("div");
- composer.className = "funpack-scene-composer";
- const editor = document.createElement("div");
- editor.className = "funpack-scene-rich-prompt";
- editor.contentEditable = "true";
- editor.spellcheck = false;
- editor.role = "textbox";
- editor.ariaMultiline = "true";
- editor.dataset.placeholder = isNegative ? "Write negative prompt words and phrases here." : "Write positive prompt words and phrases here.";
- let promptValue = normalizePromptSpacing(getWidgetValue(node, widgetName, "") || "");
- let renderingPrompt = false;
- let savedEditorRange = null;
-
- const saveEditorRange = () => {
- const selection = window.getSelection();
- if (!selection || !selection.rangeCount || !editor.contains(selection.anchorNode)) {
- return;
- }
- savedEditorRange = selection.getRangeAt(0).cloneRange();
- };
-
- const setPromptValue = (value, options = {}) => {
- promptValue = normalizePromptSpacing(value);
- setWidgetValue(node, widgetName, promptValue);
- setDirty(node);
- updateUsedChips();
- if (options.render) {
- renderRichPrompt(Boolean(options.keepFocus), options.caretOffset);
- }
- };
-
- const renderRichPrompt = (keepFocus = false, caretOffset = null) => {
- renderingPrompt = true;
- editor.replaceChildren();
- const ranges = promptPhraseRanges(promptValue, promptMemoryPhrases(isNegative));
- let cursor = 0;
- for (const range of ranges) {
- if (range.start > cursor) {
- editor.append(document.createTextNode(promptValue.slice(cursor, range.start)));
- }
- const token = document.createElement("button");
- token.type = "button";
- token.className = "funpack-scene-inline-token";
- token.contentEditable = "false";
- token.dataset.phrase = range.phrase;
- token.textContent = range.label;
- token.title = `Remove ${range.phrase}`;
- token.addEventListener("click", (event) => {
- event.preventDefault();
- event.stopPropagation();
- setPromptValue(removePhraseFromPrompt(promptValue, range.phrase), { render: true, keepFocus: true });
- });
- editor.append(token);
- cursor = range.end;
- }
- if (cursor < promptValue.length) {
- editor.append(document.createTextNode(promptValue.slice(cursor)));
- }
- ensureTrailingCaretTarget(editor);
- renderingPrompt = false;
- if (keepFocus) {
- if (Number.isFinite(Number(caretOffset))) {
- setCaretByTextOffset(editor, Number(caretOffset));
- } else {
- setCaretToEnd(editor);
- }
- saveEditorRange();
- }
- };
-
- editor.addEventListener("input", () => {
- if (renderingPrompt) {
- return;
- }
- setPromptValue(richPromptText(editor), { render: false });
- saveEditorRange();
- });
- editor.addEventListener("keyup", saveEditorRange);
- editor.addEventListener("mouseup", (event) => {
- placeCaretAfterLastTokenFromMouse(editor, event);
- saveEditorRange();
- });
- editor.addEventListener("focus", saveEditorRange);
- editor.addEventListener("keydown", (event) => {
- if (event.key === "ArrowRight") {
- const token = adjacentInlineToken(editor, "forward");
- if (token?.matches?.(".funpack-scene-inline-token")) {
- event.preventDefault();
- setCaretAfterInlineToken(token);
- saveEditorRange();
- }
- return;
- }
- if (event.key !== "Backspace" && event.key !== "Delete") {
- return;
- }
- const token = adjacentInlineToken(editor, event.key === "Backspace" ? "backward" : "forward");
- if (!token?.dataset?.phrase) {
- return;
- }
- event.preventDefault();
- if (editInlineTokenAsText(editor, token, event.key === "Backspace" ? "backward" : "forward")) {
- setPromptValue(richPromptText(editor), { render: false });
- saveEditorRange();
- }
- });
- editor.addEventListener("blur", (event) => {
- saveEditorRange();
- if (panel.root.contains(event.relatedTarget)) {
- return;
- }
- renderRichPrompt(false);
- });
- editor.addEventListener("dragover", (event) => event.preventDefault());
- editor.addEventListener("drop", (event) => {
- event.preventDefault();
- const text = event.dataTransfer?.getData("text/plain");
- if (text) {
- const caretOffset = insertIntoRichPrompt(editor, text, savedEditorRange);
- setPromptValue(richPromptText(editor), { render: true, keepFocus: true, caretOffset });
- }
- });
- renderRichPrompt(false);
- composer.append(editor);
- body.append(composer);
-
- const search = document.createElement("input");
- search.type = "search";
- search.placeholder = "Search phrase bank";
- search.className = "funpack-scene-search";
- const categoryFilter = panelSelect(["All", ...GROUP_ORDER], "All");
- categoryFilter.title = "Show one category or all categories.";
- const filterRow = document.createElement("div");
- filterRow.className = "funpack-scene-filter-row";
- filterRow.append(search, categoryFilter);
- body.append(filterRow);
-
- const bank = document.createElement("div");
- bank.className = "funpack-scene-bank";
- body.append(bank);
-
- const updateUsedChips = () => {
- for (const chip of bank.querySelectorAll(".funpack-scene-chip[data-phrase]")) {
- const used = promptContainsPhrase(promptValue, chip.dataset.phrase || "");
- chip.classList.toggle("used", used);
- chip.title = used ? `Click to remove: ${chip.dataset.phrase}` : (chip.dataset.phrase || chip.textContent || "");
- }
- };
-
- const renderBank = () => {
- const query = search.value.toLowerCase().trim();
- const selectedCategory = categoryFilter.value;
- bank.replaceChildren();
- const items = memoryItems()
- .filter((item) => isNegative ? item.source === "negative" || item.category === "negative" : item.source !== "negative" && item.category !== "negative")
- .filter((item) => selectedCategory === "All" || (item.category || (item.source === "negative" ? "negative" : "details")) === selectedCategory)
- .filter((item) => !query || String(item.text || "").toLowerCase().includes(query));
- const byGroup = new Map();
- for (const item of items) {
- const group = item.category || (item.source === "negative" ? "negative" : "details");
- if (!byGroup.has(group)) {
- byGroup.set(group, []);
- }
- byGroup.get(group).push(item);
- }
- for (const group of GROUP_ORDER) {
- const groupItems = byGroup.get(group) || [];
- if (!groupItems.length) {
- continue;
- }
- const title = document.createElement("div");
- title.className = "funpack-scene-category";
- title.textContent = group;
- bank.append(title);
- const chips = document.createElement("div");
- chips.className = "funpack-scene-chip-row";
- for (const item of groupItems) {
- const phrase = item.text || item.key;
- const chip = phraseButton(phrase, (text) => {
- if (promptContainsPhrase(promptValue, text)) {
- setPromptValue(removePhraseFromPrompt(promptValue, text), { render: true, keepFocus: true });
- } else {
- const caretOffset = insertIntoRichPrompt(editor, text, savedEditorRange);
- setPromptValue(richPromptText(editor), { render: true, keepFocus: true, caretOffset });
- }
- });
- chip.dataset.phrase = phrase;
- chips.append(chip);
- }
- bank.append(chips);
- }
- updateUsedChips();
- };
- search.addEventListener("input", renderBank);
- categoryFilter.addEventListener("change", renderBank);
- renderBank();
-
- const footer = document.createElement("div");
- footer.className = "funpack-scene-footer";
- const back = panelButton("Back");
- back.addEventListener("click", () => renderPanel(panel, node, "menu"));
- const preconstruct = panelButton("Preconstruct", "primary");
- preconstruct.disabled = isNegative;
- preconstruct.title = isNegative ? "Preconstruct is available for the positive prompt." : "Add database phrases that match the current intent.";
- preconstruct.addEventListener("click", () => {
- const intent = String(
- getWidgetValue(node, "intent_prompt", "") ||
- linkedTextValue(node, "intent_prompt") ||
- promptValue ||
- ""
- );
- const result = preconstructPrompt(promptValue, intent, isNegative);
- if (!result.added.length) {
- showPanelError(new Error("No matching database phrases found for the current intent."));
- return;
- }
- const caretOffset = insertIntoRichPrompt(editor, result.added.join(", "), savedEditorRange);
- setPromptValue(richPromptText(editor), { render: true, keepFocus: true, caretOffset });
- });
- const cancel = panelButton("Cancel");
- cancel.addEventListener("click", () => {
- setWidgetValue(node, widgetName, panel.snapshots[snapshotKey]);
- setDirty(node);
- delete panel.snapshots[snapshotKey];
- cancelEditor(panel, node);
- });
- const confirm = panelButton("Confirm", "primary");
- confirm.addEventListener("click", async () => {
- try {
- if (!activeSceneName(node)) {
- renderPanel(panel, node, "create", { nextView: kind, cancelTarget: panel.cancelTarget });
- return;
- }
- setWidgetValue(node, widgetName, promptValue);
- await saveSceneFromNode(node);
- delete panel.snapshots[snapshotKey];
- renderPanel(panel, node, "menu");
- } catch (error) {
- showPanelError(error);
- }
- });
- footer.append(back, preconstruct, cancel, confirm);
- body.append(footer);
- editor.focus();
- setCaretToEnd(editor);
-}
-
-function renderDatabaseEditor(panel, node) {
- if (!("database_snapshot" in panel.snapshots)) {
- panel.snapshots.database_snapshot = JSON.parse(JSON.stringify(memoryItems()));
- panel.databaseItems = JSON.parse(JSON.stringify(memoryItems()));
- }
-
- const body = shell(panel.root, "Database", "database");
- const tools = document.createElement("div");
- tools.className = "funpack-scene-db-tools";
- const addText = document.createElement("input");
- addText.placeholder = "Add word or phrase";
- const addCategory = document.createElement("select");
- for (const category of GROUP_ORDER) {
- const option = document.createElement("option");
- option.value = category;
- option.textContent = category;
- addCategory.append(option);
- }
- const addWildcard = panelCheckbox(false, "Wildcard");
- const add = panelButton("Add", "primary");
- add.addEventListener("click", () => {
- const text = addText.value.trim();
- if (!text) {
- return;
- }
- panel.databaseItems.unshift({
- text,
- key: text.toLowerCase(),
- source: addCategory.value === "negative" ? "negative" : "positive",
- category: addCategory.value,
- count: 0,
- wildcard: addWildcard.input.checked,
- });
- addText.value = "";
- addWildcard.input.checked = false;
- renderPanel(panel, node, "database");
- });
- tools.append(addText, addCategory, addWildcard.wrapper, add);
- body.append(tools);
-
- const search = document.createElement("input");
- search.type = "search";
- search.placeholder = "Search database";
- search.className = "funpack-scene-search";
- body.append(search);
-
- const list = document.createElement("div");
- list.className = "funpack-scene-db-list";
- body.append(list);
-
- const renderList = () => {
- const query = search.value.toLowerCase().trim();
- list.replaceChildren();
- const items = panel.databaseItems.filter((item) => !query || String(item.text || "").toLowerCase().includes(query));
- items.forEach((item) => {
- const row = document.createElement("div");
- row.className = "funpack-scene-db-row";
- const text = editableTextLabel(item.text || "", (value) => {
- item.text = value;
- item.key = value.toLowerCase();
- }, { multiline: true });
- const category = document.createElement("select");
- for (const categoryName of GROUP_ORDER) {
- const option = document.createElement("option");
- option.value = categoryName;
- option.textContent = categoryName;
- option.selected = categoryName === item.category;
- category.append(option);
- }
- category.addEventListener("change", () => {
- item.category = category.value;
- item.source = category.value === "negative" ? "negative" : "positive";
- });
- 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.wrapper, del);
- list.append(row);
- });
- };
- search.addEventListener("input", renderList);
- renderList();
-
- const footer = document.createElement("div");
- footer.className = "funpack-scene-footer";
- const back = panelButton("Back");
- back.addEventListener("click", () => renderPanel(panel, node, "menu"));
- const cancel = panelButton("Cancel");
- cancel.addEventListener("click", () => {
- panel.databaseItems = JSON.parse(JSON.stringify(panel.snapshots.database_snapshot));
- delete panel.snapshots.database_snapshot;
- cancelEditor(panel, node);
- });
- const confirm = panelButton("Confirm", "primary");
- confirm.addEventListener("click", async () => {
- try {
- await saveDatabase(node, panel.databaseItems);
- delete panel.snapshots.database_snapshot;
- renderPanel(panel, node, "menu");
- } catch (error) {
- showPanelError(error);
- }
- });
- footer.append(back, cancel, confirm);
- body.append(footer);
-}
-
-function splitLines(value) {
- return String(value || "")
- .split(/\n+/)
- .map((item) => item.trim())
- .filter(Boolean);
-}
-
-function renderShortcutEditor(panel, node) {
- const body = shell(panel.root, "Shortcuts", "shortcuts");
-
- const tools = document.createElement("div");
- tools.className = "funpack-scene-shortcut-tools";
- const add = panelButton("Add shortcut", "primary");
- add.addEventListener("click", () => {
- panel.shortcutDrafts = panel.shortcutDrafts || JSON.parse(JSON.stringify(shortcutItems()));
- panel.shortcutDrafts.unshift({
- name: "",
- enabled: true,
- triggers: [],
- replacements: [],
- });
- renderPanel(panel, node, "shortcuts");
- });
- const refresh = panelButton("Refresh");
- refresh.addEventListener("click", async () => {
- await fetchShortcuts();
- delete panel.shortcutDrafts;
- renderPanel(panel, node, "shortcuts");
- });
- const imp = panelButton("Import");
- imp.addEventListener("click", () => importShortcuts(() => {
- delete panel.shortcutDrafts;
- renderPanel(panel, node, "shortcuts");
- }));
- const exp = panelButton("Export");
- exp.addEventListener("click", async () => {
- try {
- await exportShortcuts();
- } catch (error) {
- showPanelError(error);
- }
- });
- tools.append(add, refresh, imp, exp);
- body.append(tools);
-
- if (!panel.shortcutDrafts) {
- panel.shortcutDrafts = JSON.parse(JSON.stringify(shortcutItems()));
- }
-
- const list = document.createElement("div");
- list.className = "funpack-scene-shortcut-list";
- body.append(list);
-
- const drafts = panel.shortcutDrafts;
- if (!drafts.length) {
- const empty = document.createElement("div");
- empty.className = "funpack-scene-muted";
- empty.textContent = "No shortcuts yet.";
- list.append(empty);
- }
-
- drafts.forEach((item, index) => {
- const row = document.createElement("div");
- row.className = "funpack-scene-shortcut-row";
-
- const top = document.createElement("div");
- top.className = "funpack-scene-shortcut-top";
- const name = panelTextInput(item.name || "", "Shortcut name");
- name.addEventListener("input", () => { item.name = name.value; });
- const enabled = panelCheckbox(item.enabled !== false, "Enabled");
- enabled.input.addEventListener("change", () => { item.enabled = enabled.input.checked; });
- top.append(name, enabled.wrapper);
-
- const triggers = document.createElement("textarea");
- triggers.className = "funpack-scene-edit-textarea";
- triggers.placeholder = "Activation phrases, one per line";
- triggers.value = (item.triggers || []).join("\n");
- triggers.addEventListener("input", () => { item.triggers = splitLines(triggers.value); });
-
- const replacements = document.createElement("textarea");
- replacements.className = "funpack-scene-edit-textarea";
- replacements.placeholder = "Replacement phrases, one per line";
- replacements.value = (item.replacements || []).join("\n");
- replacements.addEventListener("input", () => { item.replacements = splitLines(replacements.value); });
-
- const actions = document.createElement("div");
- actions.className = "funpack-scene-shortcut-actions";
- const save = panelButton("Save", "primary");
- save.addEventListener("click", async () => {
- try {
- await saveShortcut({
- original_name: item.key || item.name,
- name: item.name,
- enabled: item.enabled !== false,
- triggers: splitLines(triggers.value),
- replacements: splitLines(replacements.value),
- });
- await fetchShortcuts();
- delete panel.shortcutDrafts;
- renderPanel(panel, node, "shortcuts");
- } catch (error) {
- showPanelError(error);
- }
- });
- const del = panelButton("Delete", "danger");
- del.addEventListener("click", async () => {
- try {
- if (item.key || item.name) {
- await deleteShortcut(item.name || item.key);
- await fetchShortcuts();
- }
- drafts.splice(index, 1);
- delete panel.shortcutDrafts;
- renderPanel(panel, node, "shortcuts");
- } catch (error) {
- showPanelError(error);
- }
- });
- actions.append(save, del);
-
- row.append(top, triggers, replacements, actions);
- list.append(row);
- });
-
- const footer = document.createElement("div");
- footer.className = "funpack-scene-footer";
- const back = panelButton("Back");
- back.addEventListener("click", () => {
- delete panel.shortcutDrafts;
- renderPanel(panel, node, "menu");
- });
- footer.append(back);
- body.append(footer);
-}
-
-function openPanel(node, view = "menu") {
- closePanel();
- injectStyles();
- const root = document.createElement("div");
- root.className = "funpack-scene-panel";
- document.body.append(root);
- activePanel = {
- root,
- snapshots: {},
- databaseItems: [],
- cancelTarget: view === "menu" ? "menu" : "close",
- };
- const viewportWidth = Number(window.innerWidth);
- const viewportHeight = Number(window.innerHeight);
- if (Number.isFinite(viewportWidth) && Number.isFinite(viewportHeight) && viewportWidth > 0 && viewportHeight > 0) {
- root.style.left = "50%";
- root.style.top = "50%";
- root.style.transform = "translate(-50%, -50%)";
- } else {
- const canvasRect = app.canvas?.canvas?.getBoundingClientRect?.();
- const scale = app.canvas?.ds?.scale || 1;
- const offset = app.canvas?.ds?.offset || [0, 0];
- const screenX = (canvasRect?.left || 0) + (node.pos[0] + node.size[0]) * scale + offset[0];
- const screenY = (canvasRect?.top || 0) + node.pos[1] * scale + offset[1];
- const left = Math.min(Math.max(12, window.innerWidth - 460), Math.max(12, screenX + 16));
- const top = Math.min(Math.max(12, window.innerHeight - 620), Math.max(12, screenY));
- root.style.left = `${left}px`;
- root.style.top = `${top}px`;
- root.style.transform = "";
- }
- void refreshNode(node).catch((error) => {
- console.warn("FunPack: failed to refresh scenes before opening Scene Builder", error);
- }).finally(() => {
- if (activePanel?.root === root) {
- navigatePanel(activePanel, node, view, { cancelTarget: activePanel.cancelTarget });
- }
- });
-}
-
-let stylesInjected = false;
-function injectStyles() {
- if (stylesInjected) {
- return;
- }
- stylesInjected = true;
- const style = document.createElement("style");
- style.textContent = `
- .funpack-scene-panel {
- position: fixed;
- z-index: 10000;
- width: min(448px, calc(100vw - 24px));
- max-height: min(620px, calc(100vh - 24px));
- display: flex;
- flex-direction: column;
- padding: 10px;
- border: 1px solid rgba(180, 190, 200, 0.35);
- border-radius: 8px;
- background: rgba(30, 32, 36, 0.98);
- box-shadow: 0 18px 48px rgba(0, 0, 0, 0.42);
- color: #ddd;
- font: 12px sans-serif;
- box-sizing: border-box;
- }
- .funpack-scene-panel * { box-sizing: border-box; }
- .funpack-scene-panel-header,
- .funpack-scene-footer {
- display: flex;
- align-items: center;
- gap: 8px;
- justify-content: space-between;
- flex-wrap: wrap;
- }
- .funpack-scene-panel-header {
- font-weight: 700;
- font-size: 14px;
- padding-bottom: 8px;
- border-bottom: 1px solid rgba(255,255,255,0.08);
- }
- .funpack-scene-body {
- overflow: auto;
- padding-top: 8px;
- min-height: 0;
- }
- .funpack-scene-error {
- min-height: 16px;
- color: #ff9f9f;
- padding-top: 5px;
- }
- .funpack-scene-button {
- min-height: 26px;
- padding: 4px 10px;
- border: 1px solid rgba(180, 190, 200, 0.35);
- border-radius: 5px;
- background: #22252a;
- color: #eee;
- cursor: pointer;
- white-space: nowrap;
- }
- .funpack-scene-button:hover { background: #2b3037; }
- .funpack-scene-button.primary { border-color: rgba(100, 210, 140, 0.6); background: #244832; }
- .funpack-scene-button.danger { border-color: rgba(255, 130, 130, 0.45); background: #472626; }
- .funpack-scene-button.compact {
- min-height: 28px;
- padding: 4px 7px;
- }
- .funpack-scene-button.large {
- width: 100%;
- height: 42px;
- text-align: left;
- font-weight: 700;
- }
- .funpack-scene-current {
- display: grid;
- gap: 5px;
- margin-bottom: 10px;
- }
- .funpack-scene-controls {
- display: grid;
- gap: 7px;
- margin-bottom: 10px;
- }
- .funpack-scene-controls label {
- display: grid;
- grid-template-columns: 86px minmax(0, 1fr);
- gap: 8px;
- align-items: center;
- }
- .funpack-scene-create-row {
- display: grid;
- grid-template-columns: 86px minmax(0, 1fr);
- gap: 8px;
- align-items: center;
- margin-top: 8px;
- }
- .funpack-scene-controls span {
- color: #b8c0ca;
- }
- .funpack-scene-current div {
- display: grid;
- grid-template-columns: 78px minmax(0, 1fr);
- gap: 8px;
- }
- .funpack-scene-current span {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #b8c0ca;
- }
- .funpack-scene-menu-buttons,
- .funpack-scene-saved {
- display: grid;
- gap: 7px;
- }
- .funpack-scene-section-title,
- .funpack-scene-category {
- margin: 12px 0 6px;
- color: #58a6d6;
- font-weight: 700;
- text-transform: uppercase;
- }
- .funpack-scene-textarea {
- width: 100%;
- min-height: 156px;
- resize: vertical;
- padding: 8px;
- border: 1px solid rgba(180, 190, 200, 0.35);
- border-radius: 6px;
- background: #17191d;
- color: #f4f4f4;
- line-height: 1.35;
- outline: none;
- }
- .funpack-scene-search,
- .funpack-scene-controls input,
- .funpack-scene-controls select,
- .funpack-scene-create-row input,
- .funpack-scene-db-tools input,
- .funpack-scene-db-tools select,
- .funpack-scene-editable input,
- .funpack-scene-db-row input,
- .funpack-scene-db-row select {
- width: 100%;
- min-height: 28px;
- padding: 5px 7px;
- border: 1px solid rgba(180, 190, 200, 0.28);
- border-radius: 5px;
- background: #17191d;
- 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-editable {
- min-width: 0;
- }
- .funpack-scene-editable.editing {
- grid-column: 1 / -1;
- }
- .funpack-scene-inline-editor {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto auto;
- gap: 5px;
- align-items: center;
- }
- .funpack-scene-inline-editor.multiline {
- grid-template-columns: minmax(0, 1fr);
- align-items: stretch;
- }
- .funpack-scene-inline-actions {
- display: flex;
- justify-content: flex-end;
- gap: 6px;
- }
- .funpack-scene-edit-textarea {
- width: 100%;
- min-height: 92px;
- padding: 7px 8px;
- border: 1px solid rgba(180, 190, 200, 0.32);
- border-radius: 5px;
- background: #17191d;
- color: #f2f2f2;
- line-height: 1.35;
- resize: vertical;
- outline: none;
- }
- .funpack-scene-editable button {
- width: 100%;
- min-height: 28px;
- padding: 5px 7px;
- border: 1px solid transparent;
- border-radius: 5px;
- background: transparent;
- color: #f2f2f2;
- text-align: left;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- cursor: default;
- }
- .funpack-scene-editable button:hover,
- .funpack-scene-editable button:focus {
- border-color: rgba(180, 190, 200, 0.28);
- background: #17191d;
- outline: none;
- }
- .funpack-scene-composer {
- min-height: 170px;
- padding: 7px;
- border: 1px solid rgba(255,255,255,0.14);
- border-radius: 6px;
- background: #101216;
- }
- .funpack-scene-composer:focus-within {
- border-color: rgba(100, 210, 140, 0.55);
- box-shadow: 0 0 0 1px rgba(100, 210, 140, 0.12);
- }
- .funpack-scene-rich-prompt {
- width: 100%;
- min-height: 154px;
- padding: 0;
- border: 0;
- background: transparent;
- color: #f4f4f4;
- line-height: 1.45;
- outline: none;
- white-space: pre-wrap;
- overflow-wrap: anywhere;
- cursor: text;
- }
- .funpack-scene-rich-prompt:empty::before {
- content: attr(data-placeholder);
- color: #7f8792;
- pointer-events: none;
- }
- .funpack-scene-inline-token {
- display: inline-flex;
- align-items: center;
- max-width: 100%;
- margin: 0 2px 3px;
- padding: 1px 6px;
- border: 1px solid rgba(100, 210, 140, 0.75);
- border-radius: 5px;
- background: rgba(58, 132, 86, 0.42);
- color: #eaffef;
- font: inherit;
- line-height: 1.25;
- vertical-align: baseline;
- cursor: pointer;
- white-space: normal;
- text-align: left;
- }
- .funpack-scene-inline-token:hover,
- .funpack-scene-inline-token:focus {
- background: rgba(73, 158, 102, 0.56);
- outline: none;
- }
- .funpack-scene-inline-token::after {
- content: " ×";
- padding-left: 4px;
- color: #bff4cd;
- font-weight: 700;
- }
- .funpack-scene-search { margin: 8px 0; }
- .funpack-scene-filter-row {
- display: grid;
- grid-template-columns: minmax(0, 1fr) 135px;
- gap: 8px;
- align-items: center;
- }
- .funpack-scene-bank,
- .funpack-scene-db-list {
- max-height: 270px;
- overflow: auto;
- padding-right: 3px;
- }
- .funpack-scene-chip-row {
- display: flex;
- flex-wrap: wrap;
- gap: 5px;
- }
- .funpack-scene-chip {
- max-width: 100%;
- padding: 4px 8px;
- border: 1px solid rgba(255,255,255,0.14);
- border-radius: 6px;
- background: rgba(255,255,255,0.07);
- color: #eee;
- cursor: grab;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .funpack-scene-chip.used {
- border-color: rgba(100, 210, 140, 0.75);
- background: rgba(58, 132, 86, 0.42);
- color: #eaffef;
- }
- .funpack-scene-chip.used::after {
- content: " used";
- color: #a8efbf;
- font-size: 10px;
- font-weight: 700;
- text-transform: uppercase;
- }
- .funpack-scene-db-tools {
- display: grid;
- 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 auto auto;
- gap: 6px;
- align-items: center;
- padding: 5px 0;
- border-bottom: 1px solid rgba(255,255,255,0.06);
- }
- .funpack-scene-shortcut-tools,
- .funpack-scene-shortcut-actions {
- display: flex;
- gap: 6px;
- flex-wrap: wrap;
- justify-content: flex-end;
- margin-bottom: 8px;
- }
- .funpack-scene-shortcut-list {
- display: grid;
- gap: 10px;
- max-height: 430px;
- overflow: auto;
- padding-right: 3px;
- }
- .funpack-scene-shortcut-row {
- display: grid;
- gap: 7px;
- padding: 8px;
- border: 1px solid rgba(255,255,255,0.08);
- border-radius: 6px;
- background: rgba(255,255,255,0.035);
- }
- .funpack-scene-shortcut-top {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 8px;
- align-items: center;
- }
- .funpack-scene-footer {
- margin-top: 10px;
- padding-top: 8px;
- border-top: 1px solid rgba(255,255,255,0.08);
- justify-content: flex-end;
- }
- .funpack-scene-muted { color: #9da6b0; }
- `;
- document.head.append(style);
-}
-
-class SceneBuilderWidget {
- constructor(node) {
- this.node = node;
- this.name = "funpack_scene_builder";
- this.type = "custom";
- this.options = { serialize: false };
- this.value = "";
- this.hitAreas = {};
- }
-
- computeSize(width) {
- return [width, 82];
- }
-
- draw(ctx, node, width, y) {
- this.hitAreas = {};
- const margin = 10;
- let cy = y + 4;
- const innerWidth = width - margin * 2;
- const mode = getWidgetValue(node, "mode", "Manual");
- const scene = String(getWidgetValue(node, "scene_name", "") || "Unnamed scene").trim();
-
- ctx.save();
- ctx.beginPath();
- ctx.rect(0, y, width, this.computeSize(width)[1]);
- ctx.clip();
- ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
- ctx.textBaseline = "middle";
- ctx.textAlign = "left";
- ctx.font = "12px sans-serif";
- ctx.fillText(fitString(ctx, `Scene Builder: ${scene}`, width - 134), margin, cy + 9);
- button(ctx, this, "open:menu", "Open Editor", width - 112, cy, 92, 20);
- cy += 28;
-
- const third = Math.floor((innerWidth - 12) / 3);
- button(ctx, this, "open:positive", "Positive", margin, cy, third, 28);
- button(ctx, this, "open:negative", "Negative", margin + third + 6, cy, third, 28);
- button(ctx, this, "open:database", "Database", margin + (third + 6) * 2, cy, innerWidth - (third + 6) * 2, 28);
- ctx.globalAlpha = app.canvas.editor_alpha * 0.62;
- ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
- ctx.fillText(`${mode}. ${memoryItems().length} database phrase(s). Connected prompts learn on queue.`, margin, cy + 42);
- ctx.globalAlpha = app.canvas.editor_alpha;
- ctx.restore();
- }
-
- mouse(event, pos, node) {
- if (event.type !== "pointerup") {
- return event.type === "pointerdown" && this.hitAt(pos);
- }
- const hit = Object.entries(this.hitAreas).find(([, bounds]) => (
- pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[2] &&
- pos[1] >= bounds[1] && pos[1] <= bounds[1] + bounds[3]
- ));
- if (!hit) {
- return false;
- }
- const [, view] = hit[0].split(":");
- openPanel(node, view || "menu");
- return true;
- }
-
- hitAt(pos) {
- return Object.values(this.hitAreas).some((bounds) => (
- pos[0] >= bounds[0] && pos[0] <= bounds[0] + bounds[2] &&
- pos[1] >= bounds[1] && pos[1] <= bounds[1] + bounds[3]
- ));
- }
-}
-
-function removeSceneBuilderWidget(node) {
- node.widgets = (node.widgets || []).filter((widget) => widget.name !== "funpack_scene_builder");
-}
-
-function setupSceneBuilderNode(node) {
- trackedNodes.add(node);
- const hideInternalWidgets = () => {
- for (const widget of node.widgets || []) {
- if (HIDDEN_WIDGETS.has(widget.name)) {
- hideWidget(widget);
- }
- }
- setDirty(node);
- };
- hideInternalWidgets();
- window.requestAnimationFrame?.(hideInternalWidgets);
- window.setTimeout(hideInternalWidgets, 0);
- window.setTimeout(hideInternalWidgets, 250);
- removeSceneBuilderWidget(node);
- node.addCustomWidget(new SceneBuilderWidget(node));
- updateSceneCombo(node);
- void fetchScenes(currentRefinementKey(node)).then(() => {
- updateSceneCombo(node);
- setDirty(node);
- });
-}
-
-function wrapRefreshFunction(functionName) {
- const original = app[functionName];
- if (typeof original !== "function" || original.__funpackSceneBuilderWrapped) {
- return;
- }
- const wrapped = function () {
- const result = original.apply(this, arguments);
- Promise.resolve(result).finally(() => scheduleRefresh());
- return result;
- };
- wrapped.__funpackSceneBuilderWrapped = true;
- app[functionName] = wrapped;
-}
-
-function hookComfyRefreshControls() {
- for (const functionName of ["refreshComboInNodes", "refreshComboInNode", "refreshNodeDefs"]) {
- wrapRefreshFunction(functionName);
- }
-}
-
-app.registerExtension({
- name: "funpack.sceneBuilder",
- setup() {
- hookComfyRefreshControls();
- window.setTimeout(hookComfyRefreshControls, 0);
- window.setTimeout(hookComfyRefreshControls, 1000);
- },
- beforeRegisterNodeDef(nodeType, nodeData) {
- if (nodeData.name !== NODE_NAME) {
- return;
- }
- hookComfyRefreshControls();
- const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
- nodeType.prototype.onNodeCreated = function () {
- originalOnNodeCreated?.apply(this, arguments);
- setupSceneBuilderNode(this);
- };
- const originalConfigure = nodeType.prototype.configure;
- nodeType.prototype.configure = function (info) {
- originalConfigure?.apply(this, arguments);
- setupSceneBuilderNode(this);
- };
- },
-});
diff --git a/web/funpack_studio.js b/web/funpack_studio.js
index 9c24a325..c0a06015 100644
--- a/web/funpack_studio.js
+++ b/web/funpack_studio.js
@@ -9,17 +9,14 @@ const ADVISOR_DTYPES = ["bfloat16", "float16", "float32"];
const REFINER_MODES = ["Refine", "Prompt only", "Learning"];
const ADVISOR_MODES = ["Off", "Only diagnostics", "Only prompt", "Full"];
const TEMPORAL_STYLES = ["natural", "auto", "accelerate", "decelerate", "loop", "freeze", "pulse"];
-const SB_MODES = ["Pass-through", "Manual", "Auto", "Learning"];
const CATEGORY_ORDER = ["action", "camera", "subject", "appearance", "environment", "style", "quality", "details"];
-const TABS = ["Session", "Scene", "Shortcuts", "Split markers", "Refiner", "Advisor", "LoRA", "Sampler", "Adjustments", "Timeline"];
+const TABS = ["Session", "Shortcuts", "Split markers", "Refiner", "Advisor", "LoRA", "Sampler", "Adjustments", "Timeline"];
const SAMPLER_TYPES = ["Hybrid Euler 2S", "Distilled Flow", "Normalizing", "KSampler"];
const MOTION_PULSE_MODES = ["off", "balanced", "aggressive", "custom"];
const VELOCITY_BIAS_MODES = ["off", "capture", "apply", "capture_and_apply"];
const KSAMPLER_NAMES = ["euler", "euler_ancestral", "dpm_2", "dpm_2_ancestral", "dpmpp_2m", "dpmpp_sde", "ddim", "uni_pc"];
let activePanel = null;
-let studioSceneData = null;
-let studioSceneKey = null;
let studioShortcutData = null;
let studioTransitionData = null;
@@ -68,7 +65,6 @@ function defaultSettings() {
return {
refinement_key: "",
overrides: { refinement_key: false, feedback_prompt: false, user_intent_prompt: false, negative_prompt: false },
- scene_builder: { mode: "Pass-through", scene: NONE_SENTINEL, scene_name: "", aliases: "", scene_positive: "", scene_negative: "" },
refiner: { mode: "Refine", advisor_mode: "Off", advisor_thinking: true, im_feeling_lucky: false, reset_session: false, feedback_prompt: "", user_intent_prompt_override: "", negative_prompt: "", temporal_style: "natural", split_by_transitions: false, split_transition_placement: "start", reference_injection: false, vision_conditioning: true, value_guidance: true, steer_mode: "relative", absolute_strength: 0.6 },
advisor_llm: { enabled: false, model_path: "huihui-ai/Huihui-Qwen3-8B-abliterated-v2", dtype: "bfloat16" },
loras: [],
@@ -118,27 +114,6 @@ function hideWidget(widget) {
// ─── API calls ────────────────────────────────────────────────────────────────
-async function fetchScenes(key = "") {
- try {
- const params = new URLSearchParams({ cache_bust: Date.now() });
- if (key) params.set("key", key);
- const res = await api.fetchApi(`/funpack/scenes?${params}`, { cache: "no-store" });
- if (!res.ok) return null;
- studioSceneData = await res.json();
- studioSceneKey = key || "";
- return studioSceneData;
- } catch { return null; }
-}
-
-async function saveScene(key, payload) {
- const params = key ? `?key=${encodeURIComponent(key)}` : "";
- const res = await api.fetchApi(`/funpack/scenes/scene${params}`, {
- method: "POST", headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action: "save", ...payload }),
- });
- if (!res.ok) { const e = await res.json().catch(() => ({})); throw new Error(e.error || `HTTP ${res.status}`); }
-}
-
async function fetchShortcuts() {
try {
const params = new URLSearchParams({ cache_bust: Date.now() });
@@ -741,7 +716,6 @@ function openPanel(node) {
body.replaceChildren();
errorEl.textContent = "";
if (name === "Session") renderSession();
- else if (name === "Scene") renderScene();
else if (name === "Shortcuts") renderShortcuts();
else if (name === "Split markers") renderSplitMarkers();
else if (name === "Refiner") renderRefiner();
@@ -759,8 +733,6 @@ function openPanel(node) {
const keyInput = textInput(settings.refinement_key, "session key name");
keyInput.addEventListener("input", () => {
settings.refinement_key = keyInput.value.trim();
- studioSceneData = null;
- studioSceneKey = null;
});
const linkedKey = linkedRefinementKey(node);
@@ -776,153 +748,6 @@ function openPanel(node) {
const resetToggle = toggleEl(settings.refiner.reset_session, "Reset session on next run");
resetToggle.inp.addEventListener("change", () => { settings.refiner.reset_session = resetToggle.inp.checked; });
body.append(row("Reset", resetToggle.wrap));
-
- body.append(sectionTitle("Scene Builder"));
- const modeSelect = selectEl(SB_MODES, settings.scene_builder.mode);
- modeSelect.addEventListener("change", async () => {
- settings.scene_builder.mode = modeSelect.value;
- saveSettings(node, settings);
- studioSceneData = null;
- studioSceneKey = null;
- if (settings.scene_builder.mode !== "Pass-through") {
- await fetchScenes(settings.refinement_key || linkedRefinementKey(node));
- }
- renderTab(activeTab);
- });
- body.append(row("Mode", modeSelect));
-
- if (settings.scene_builder.mode !== "Pass-through") {
- const hint = el("div", "funpack-studio-hint",
- "Scene Builder active - prompt is built from the Scene tab, not from the positive_prompt input.");
- body.append(hint);
- const toScene = btn("Open Scene tab →", "secondary");
- toScene.addEventListener("click", () => switchTab("Scene"));
- body.append(toScene);
- } else {
- const hint = el("div", "funpack-studio-hint",
- "Pass-through - the positive_prompt connected to the node is used as-is.");
- body.append(hint);
- }
- }
-
- // SCENE ────────────────────────────────────────────────────────────────────
- function renderScene() {
- body.append(sectionTitle("Scene Builder"));
-
- if (settings.scene_builder.mode === "Pass-through") {
- body.append(el("div", "funpack-studio-hint",
- "Scene Builder is in Pass-through mode. Change mode in the Session tab to enable scene construction."));
- return;
- }
-
- const key = settings.refinement_key || linkedRefinementKey(node);
-
- // Scene name + scene selector
- const sceneNames = studioSceneData?.scenes || [NONE_SENTINEL];
- const sceneSelect = selectEl(sceneNames, settings.scene_builder.scene || NONE_SENTINEL);
- sceneSelect.addEventListener("change", () => {
- settings.scene_builder.scene = sceneSelect.value;
- const scene = studioSceneData?.data?.scenes?.[sceneSelect.value];
- if (scene) {
- settings.scene_builder.scene_name = scene.name || sceneSelect.value;
- settings.scene_builder.aliases = Array.isArray(scene.aliases) ? scene.aliases.join(", ") : "";
- settings.scene_builder.scene_positive = scene.positive_text || "";
- settings.scene_builder.scene_negative = scene.negative_text || "";
- renderScene();
- }
- });
- body.append(row("Saved scene", sceneSelect));
-
- const nameInput = textInput(settings.scene_builder.scene_name, "Scene name");
- nameInput.addEventListener("input", () => { settings.scene_builder.scene_name = nameInput.value; });
- body.append(row("Scene name", nameInput));
-
- const aliasInput = textInput(settings.scene_builder.aliases, "Aliases, comma separated");
- aliasInput.addEventListener("input", () => { settings.scene_builder.aliases = aliasInput.value; });
- body.append(row("Aliases", aliasInput));
-
- body.append(sectionTitle("Positive prompt"));
- const posArea = el("textarea", "funpack-studio-textarea");
- posArea.value = settings.scene_builder.scene_positive || "";
- posArea.placeholder = "Positive prompt phrases...";
- posArea.addEventListener("input", () => { settings.scene_builder.scene_positive = posArea.value; });
- body.append(posArea);
-
- // Memory phrase bank
- const memItems = studioSceneData?.memory || [];
- if (memItems.length) {
- body.append(sectionTitle("Phrase bank - click to insert"));
- const search = el("input", "funpack-studio-search");
- search.type = "search";
- search.placeholder = "Search phrases";
- body.append(search);
-
- const bank = el("div", "funpack-studio-bank");
- const renderBank = () => {
- const q = search.value.toLowerCase().trim();
- const filtered = q ? memItems.filter((m) => String(m.text || "").toLowerCase().includes(q)) : memItems;
- bank.replaceChildren();
- const byGroup = new Map();
- for (const m of filtered) {
- const cat = CATEGORY_ORDER.includes(m.category) ? m.category : "details";
- if (!byGroup.has(cat)) byGroup.set(cat, []);
- byGroup.get(cat).push(m);
- }
- for (const cat of CATEGORY_ORDER) {
- const g = byGroup.get(cat);
- if (!g?.length) continue;
- bank.append(el("div", "funpack-studio-cat-label", cat));
- const chipRow = el("div", "funpack-studio-chip-row");
- for (const m of g) {
- const chip = el("button", "funpack-studio-chip", m.text);
- chip.type = "button";
- chip.addEventListener("click", () => {
- const cur = posArea.value.trim();
- posArea.value = cur ? `${cur}, ${m.text}` : m.text;
- settings.scene_builder.scene_positive = posArea.value;
- });
- chipRow.append(chip);
- }
- bank.append(chipRow);
- }
- };
- renderBank();
- search.addEventListener("input", renderBank);
- body.append(bank);
- }
-
- // Save/delete scene buttons
- const footer = el("div", "funpack-studio-footer");
- const refreshBtn = btn("Refresh");
- refreshBtn.addEventListener("click", async () => {
- saveSettings(node, settings);
- await fetchScenes(key);
- renderScene();
- });
- const saveBtn = btn("Save scene", "primary");
- saveBtn.addEventListener("click", async () => {
- try {
- saveSettings(node, settings);
- await saveScene(key, {
- name: settings.scene_builder.scene_name,
- aliases: settings.scene_builder.aliases,
- mode: settings.scene_builder.mode,
- positive_text: settings.scene_builder.scene_positive,
- negative_text: settings.scene_builder.scene_negative,
- });
- await fetchScenes(key);
- settings.scene_builder.scene = settings.scene_builder.scene_name || settings.scene_builder.scene || NONE_SENTINEL;
- saveSettings(node, settings);
- renderScene();
- } catch (e) { showError(root, e.message); }
- });
- footer.append(refreshBtn, saveBtn);
- body.append(footer);
-
- // Fetch scenes if not loaded
- if (!studioSceneData || studioSceneKey !== (key || "")) {
- fetchScenes(key).then(() => renderScene());
- }
}
// SHORTCUTS ────────────────────────────────────────────────────────────────
@@ -1165,15 +990,6 @@ function openPanel(node) {
// REFINER ──────────────────────────────────────────────────────────────────
function renderRefiner() {
- const sbMode = settings.scene_builder?.mode || "Pass-through";
- const sbActive = sbMode !== "Pass-through";
- if (sbActive) {
- const banner = el("div", "funpack-studio-banner");
- banner.innerHTML = `Scene Builder is active (${sbMode} mode) — positive prompt is built in the Scene tab, not from the node input.`;
- banner.querySelector("a").addEventListener("click", (e) => { e.preventDefault(); switchTab("Scene"); });
- body.append(banner);
- }
-
body.append(sectionTitle("Execution"));
const modeSelect = selectEl(REFINER_MODES, settings.refiner.mode);
@@ -1300,17 +1116,11 @@ function openPanel(node) {
body.append(fbArea);
body.append(sectionTitle("Intent"));
- if (sbActive) {
- body.append(el("div", "funpack-studio-hint",
- "Intent is derived from the Scene Builder prompt when Scene Builder is active."));
- }
body.append(overrideToggle(settings, "user_intent_prompt",
"Override - use popup value even when user_intent_prompt input is connected"));
const intentArea = el("textarea", "funpack-studio-textarea short");
intentArea.value = settings.refiner.user_intent_prompt_override || "";
intentArea.placeholder = "Intent override (overrides the user_intent_prompt node input)...";
- intentArea.disabled = sbActive;
- if (sbActive) intentArea.style.opacity = "0.45";
intentArea.addEventListener("input", () => { settings.refiner.user_intent_prompt_override = intentArea.value; });
body.append(intentArea);
}
From 0bf8cbc6b902f3d6a029ab81d191687036a17f7a Mon Sep 17 00:00:00 2001
From: digital-garbage <133010426+digital-garbage@users.noreply.github.com>
Date: Tue, 16 Jun 2026 00:13:42 +0300
Subject: [PATCH 7/7] Movie Editor: fix timeline not re-rendering after prompt
edits
onStore marked the data fingerprint as handled before checking whether
render() actually redrew. render() can decline (focus/drag/scroll), so
post-generation prompt edits left the timeline showing stale text and
the "Generated with" badge. Only commit the fingerprint when the render
succeeds, both in timeline.js's onStore and view_bus.js's subscribeZone.
---
movie_editor/frontend/timeline.js | 7 ++++---
movie_editor/frontend/view_bus.js | 3 ++-
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/movie_editor/frontend/timeline.js b/movie_editor/frontend/timeline.js
index ba972a37..fcb10f92 100644
--- a/movie_editor/frontend/timeline.js
+++ b/movie_editor/frontend/timeline.js
@@ -1978,18 +1978,19 @@
?? JSON.stringify({ sel: st.selectedSceneId, sels: st.selectedSceneIds });
const fpRat = _ratingsFingerprint(st);
if (fpData !== _lastDataFp) {
+ const ok = render(st);
+ if (!ok) return false; // render declined (focus/drag/scroll) — retry on next notify
_lastDataFp = fpData;
_lastSelFp = fpSel;
_lastRatFp = fpRat;
- const ok = render(st);
- if (_pendingAutoFit && ok && st.project) {
+ if (_pendingAutoFit && st.project) {
_pendingAutoFit = false;
if (_autoFitEnabled) {
const total = S.previewTotalSec ? S.previewTotalSec() : tlTotalSec;
requestAnimationFrame(() => { if (total > 0) fit(total); });
}
}
- return;
+ return true;
}
if (fpRat !== _lastRatFp) {
_lastRatFp = fpRat;
diff --git a/movie_editor/frontend/view_bus.js b/movie_editor/frontend/view_bus.js
index fa0f57d3..86f3dc47 100644
--- a/movie_editor/frontend/view_bus.js
+++ b/movie_editor/frontend/view_bus.js
@@ -142,8 +142,9 @@
return S.subscribe((st) => {
const fp = fpFn(st);
if (fp === last) return;
+ const ok = fn(st);
+ if (ok === false) return; // handler declined to apply this update — retry next notify
last = fp;
- fn(st);
});
}