feat: linked input "Studio · Enhanced prompt (else prompt + postfix)"

Studio gains an enhanced_prompt output; the link wires to it. Enhancer off or failed sends prompt + postfix unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-09-23 23:41:03 +03:00
co-authored by Claude Opus 5.5
parent b48a6a2ead
commit 85e5173c7c
7 changed files with 113 additions and 10 deletions
+29 -3
View File
@@ -9920,6 +9920,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
prompt_enhance_temperature=0.7, prompt_enhance_top_p=0.92,
prompt_enhance_max_length=400, prompt_enhance_thinking=False,
prompt_enhance_image=None, prompt_enhance_sampling=None,
prompt_enhance_output=False,
_seed=None, _seed_source="fresh seed", _scene_seeds=None, _velocity_keys=None,
batch_variants=1, guess_mode=False, guess_direction="up", guess_range=1.0,
guess_freeze_seed=True, movie_editor_scene_ratings=None, scene_segments=None,
@@ -10591,6 +10592,26 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner):
"scene": None, "before": _before, "after": prompt_to_encode,
"status": _enhance_status, "image": prompt_enhance_image is not None})
# Studio's `enhanced_prompt` output, for a node encoding on its own (Editor link
# "Enhanced prompt, fallback to prompt + postfix"). The same text that link's
# fallback sends — expanded prompt + postfix — enhanced once as a whole.
self._v2_enhanced_output = None
if prompt_enhance_output:
_whole = str((_link_texts or {}).get("full_prompt") or "").strip()
if prompt_enhance and _whole:
_after, _st = self._v2_enhance_prompt(
_enhance_clip, _whole, _enhance_system, cache=_enhance_cache,
seed=_enhance_seed, temperature=prompt_enhance_temperature,
top_p=prompt_enhance_top_p, max_length=prompt_enhance_max_length,
thinking=prompt_enhance_thinking, image=prompt_enhance_image,
**_enhance_sampling)
print(f"[FunPackVideoRefinerV2] Enhanced prompt output: {_st}")
self._v2_enhanced_prompts.append({
"scene": None, "before": _whole, "after": _after, "status": _st,
"image": prompt_enhance_image is not None})
_whole = _after
self._v2_enhanced_output = _whole
cond, meta, encode_status, conditioning_owner = self._v2_conditioning_source(
clip,
prompt_to_encode,
@@ -13517,8 +13538,8 @@ class FunPackStudio:
Advisor LLM, LoRA management, and Conditioning Adjust under one UI."""
CATEGORY = "FunPack"
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "INT", "SAMPLER", "SIGMAS", "SAMPLER", "SIGMAS", "IMAGE", "STRING", "STRING", "STRING", "LATENT")
RETURN_NAMES = ("model", "modified_positive", "negative", "seed", "high_pass_sampler", "high_pass_sigmas", "low_pass_sampler", "low_pass_sigmas", "loss_graph", "status", "training_info", "encoded_prompts", "video_latent")
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "INT", "SAMPLER", "SIGMAS", "SAMPLER", "SIGMAS", "IMAGE", "STRING", "STRING", "STRING", "LATENT", "STRING")
RETURN_NAMES = ("model", "modified_positive", "negative", "seed", "high_pass_sampler", "high_pass_sigmas", "low_pass_sampler", "low_pass_sigmas", "loss_graph", "status", "training_info", "encoded_prompts", "video_latent", "enhanced_prompt")
FUNCTION = "run"
DESCRIPTION = (
"FunPack Studio - all FunPack refinement tools in one node. "
@@ -13823,6 +13844,7 @@ class FunPackStudio:
h3_phrase_emphasis=bool(rf.get("h3_phrase_emphasis", False)),
h3_phrase_variability=float(rf.get("h3_phrase_variability", 0.0) or 0.0),
prompt_enhance=bool(rf.get("prompt_enhance", False)),
prompt_enhance_output=bool(rf.get("prompt_enhance_output", False)),
prompt_enhance_system=str(rf.get("prompt_enhance_system", "") or ""),
prompt_enhance_temperature=float(rf.get("prompt_enhance_temperature", 0.7)),
prompt_enhance_top_p=float(rf.get("prompt_enhance_top_p", 0.92)),
@@ -13928,7 +13950,11 @@ class FunPackStudio:
cond = _h3_reconcile_token_tags(cond, "positive")
out_negative = _h3_reconcile_token_tags(out_negative, "negative")
result = (out_model, cond, out_negative, seed, high_sampler, high_sigmas, low_sampler, low_sigmas, loss_graph, status, training_info, encoded_prompts, video_latent)
# Enhanced prompt + postfix, or the plain one when nothing enhanced it.
_whole = getattr(refiner, "_v2_enhanced_output", None)
if _whole is None:
_whole = str((rf.get("link_texts") or {}).get("full_prompt") or "")
result = (out_model, cond, out_negative, seed, high_sampler, high_sigmas, low_sampler, low_sigmas, loss_graph, status, training_info, encoded_prompts, video_latent, _whole)
enhanced = getattr(refiner, "_v2_enhanced_prompts", None)
if enhanced:
# History carries every node's ui output; the Editor's /status reads this key.
+29 -2
View File
@@ -51,6 +51,9 @@ CORE: dict[str, str] = {
# core internal links: core_id -> {input_name: (src_core_id, output_index)}
# FunPackStudio's `enhanced_prompt` output (last in RETURN_TYPES).
STUDIO_ENHANCED_PROMPT_OUT = 13
CORE_LINKS: dict[str, dict[str, tuple[str, int]]] = {
"studio": {"positive_prompt": ("pos", 0), "negative_prompt": ("neg", 0),
"refinement_key_input": ("keyloader", 0)},
@@ -820,6 +823,27 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
# linked inputs — they still receive the raw text and expand it themselves.
expanded = params.get("expanded") or {}
val = expanded[key] if key in expanded else params.get(key)
# Enhanced prompt: written by Studio's enhancer DURING the run, so it is a link to
# Studio's `enhanced_prompt` output, not a value. Studio sends prompt + postfix
# through unchanged when the enhancer is off or fails.
if key == "enhanced_prompt":
if "studio" in graph:
val = ["studio", STUDIO_ENHANCED_PROMPT_OUT]
try:
_ss_l = json.loads(str(graph["studio"]["inputs"].get("studio_settings") or "{}"))
except ValueError:
_ss_l = {}
if not isinstance(_ss_l, dict):
_ss_l = {}
_rf_l = _ss_l.get("refiner") if isinstance(_ss_l.get("refiner"), dict) else {}
_rf_l["prompt_enhance_output"] = True
_ss_l["refiner"] = _rf_l
graph["studio"]["inputs"]["studio_settings"] = json.dumps(_ss_l)
else:
val = expanded.get("full_prompt")
report["ignored"].append(
f"Linked input '{link.get('name') or key}': the built-in pipeline is off, "
f"so there is no Studio to enhance — sent prompt + postfix instead.")
# A latent node driven by "Project · Frames" has to receive the SAME number the
# sampler is given, or the two disagree about the scene length and the run dies
# on the mismatch. Same for a fixed-rate family's fps.
@@ -858,10 +882,13 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No
f"Linked input '{label}': {graph[sid].get('class_type')} has no widget called "
f"'{inp}' — nothing was set. Re-pick it in Models ▸ Linked inputs.")
continue
graph[sid]["inputs"][inp] = val
graph[sid]["inputs"][inp] = list(val) if isinstance(val, list) else val
if isinstance(val, list):
protected_edges.add((sid, inp))
applied.append(f"{graph[sid].get('class_type')}.{inp}")
if applied:
report["wired"].append(f"linked '{label}' = {val} -> " + ", ".join(applied))
shown = "Studio · enhanced_prompt" if isinstance(val, list) else val
report["wired"].append(f"linked '{label}' = {shown} -> " + ", ".join(applied))
port_to_core = _port_index(object_info, core=CORE)
+2 -1
View File
@@ -772,7 +772,8 @@
const tag = el("div", "insp-tag"); tag.textContent = "Exposed controls"; wrap.append(tag);
const SRC_LBL = { frame_rate: "Project FPS", num_frames_per_scene: "Project Frames",
width: "Project Width", height: "Project Height", prompt: "Project Prompt",
negative_prompt: "Project Negative prompt", seed: "Project Seed" };
negative_prompt: "Project Negative prompt", seed: "Project Seed",
full_prompt: "Project Prompt + postfix", enhanced_prompt: "Studio Enhanced prompt" };
linkItems.forEach((l) => {
if (l.source === "editor") {
const note = el("div", "lib-sub"); note.textContent = `← ${SRC_LBL[l.editor_key] || l.editor_key}`;
+7 -1
View File
@@ -1941,6 +1941,9 @@
{ key: "anchor", label: "Project · Anchor text (prepended)", kinds: ["string"] },
{ key: "postfix", label: "Project · Postfix (appended)", kinds: ["string"] },
{ key: "full_prompt", label: "Project · Prompt + postfix (what Studio encodes)", kinds: ["string"] },
// Written by Studio's enhancer during the run, so the builder wires it to Studio's
// enhanced_prompt output rather than pasting a value. Enhancer off → prompt + postfix.
{ key: "enhanced_prompt", label: "Studio · Enhanced prompt (else prompt + postfix)", kinds: ["string"] },
{ key: "seed", label: "Project · Seed", kinds: ["int", "float"] },
{ key: "frame_rate", label: "Project · FPS", kinds: ["int", "float"] },
{ key: "num_frames_per_scene", label: "Project · Frames", kinds: ["int", "float"] },
@@ -1995,7 +1998,7 @@
if (link.source === "editor") {
const srcLbl = (EDITOR_SOURCES.find((s) => s.key === link.editor_key) || {}).label || link.editor_key;
const isText = ["prompt", "negative_prompt", "anchor", "postfix", "full_prompt"].includes(link.editor_key);
const isText = ["prompt", "negative_prompt", "anchor", "postfix", "full_prompt", "enhanced_prompt"].includes(link.editor_key);
// "the same text Studio would" is only true of full_prompt. Studio appends the postfix
// to every scene itself, so a node linked to the global prompt encodes strictly less --
// silently, and the postfix is where audio and style directions usually live.
@@ -2004,6 +2007,9 @@
note = " Shortcuts and $variables are expanded first";
if (link.editor_key === "full_prompt") {
note += ", so the node encodes the same text Studio would.";
} else if (link.editor_key === "enhanced_prompt") {
note = " Prompt + postfix, rewritten by the enhancer (Composer ▸ Enhance). Enhancer off"
+ " or failed: prompt + postfix unchanged.";
} else if (link.editor_key === "prompt") {
const pj = window.Store?.get().project || {};
const hasPostfix = pj.postfix_enabled !== false && String(pj.postfix || "").trim();
+17
View File
@@ -1580,3 +1580,20 @@ def test_studio_receives_the_texts_linked_nodes_were_given():
settings = json.loads(graph["studio"]["inputs"]["studio_settings"])
assert settings["refiner"]["link_texts"]["prompt"] == "cinematic rain"
assert settings["refiner"]["link_texts"]["full_prompt"] == "cinematic rain now"
def test_enhanced_prompt_link_wires_to_studios_output():
import json
oi = {**OI, "R2V": {"input": {"required": {"prompt": ["STRING", {"default": ""}]}},
"output": ["CONDITIONING"], "output_name": ["CONDITIONING"]}}
oi["FunPackStudio"] = {**OI["FunPackStudio"],
"output": OI["FunPackStudio"]["output"] + ["STRING"],
"output_name": OI["FunPackStudio"]["output_name"] + ["enhanced_prompt"]}
models = {"slots": [{"id": "r2v", "node_class": "R2V", "inputs": {}, "wires": {}}],
"links": [{"id": "l", "name": "p", "source": "editor", "editor_key": "enhanced_prompt",
"members": [{"slotId": "r2v", "input": "prompt"}]}]}
graph, report = builder.build(oi, models, {**PARAMS, "expanded": {"full_prompt": "a cat, soft"}})
assert graph["slot_r2v"]["inputs"]["prompt"] == ["studio", builder.STUDIO_ENHANCED_PROMPT_OUT]
assert oi["FunPackStudio"]["output_name"][builder.STUDIO_ENHANCED_PROMPT_OUT] == "enhanced_prompt"
rf = json.loads(graph["studio"]["inputs"]["studio_settings"])["refiner"]
assert rf["prompt_enhance_output"] is True
+3 -3
View File
@@ -395,7 +395,7 @@ def test_the_image_reaches_the_enhancer():
def test_both_paths_hand_over_the_enhancer_image():
src = _refine_source()
assert src.count("image=prompt_enhance_image") == 2
assert src.count("image=prompt_enhance_image") == 3
assert "thinking=prompt_enhance_thinking, image=source_image" not in src
@@ -409,7 +409,7 @@ def test_studio_gates_the_image_on_its_own_switch():
def test_every_rewrite_is_reported_for_the_composer():
src = _refine_source()
assert src.count("self._v2_enhanced_prompts.append(") == 2
assert src.count("self._v2_enhanced_prompts.append(") == 3
import inspect
assert '{"ui": {"funpack_enhanced": enhanced}, "result": result}' in inspect.getsource(C.FunPackStudio.run)
@@ -437,7 +437,7 @@ def test_the_repair_advisor_keeps_its_old_sampling(studio):
def test_a_fixed_enhancer_seed_overrides_the_run_seed():
src = _refine_source()
assert '_enhance_seed = int(_enhance_sampling.pop("seed", 0) or 0) or seed' in src
assert src.count("seed=_enhance_seed") == 2
assert src.count("seed=_enhance_seed") == 3
# ── how the instructions reach the model ─────────────────────────────────────
+26
View File
@@ -2295,3 +2295,29 @@ def test_h3_scale_digits_are_valid_combo_choices():
whatever the default happens to be."""
for n in range(1, 11):
assert str(n) in V2_RATING_LABELS
def _enhanced_output(tmp_path, **kw):
refiner = FunPackVideoRefinerV2()
refiner._v2_state_path = lambda refinement_key: str(tmp_path / "state.json")
clip = GeneratingClip("a vivid cat on a sunlit sill")
refiner.refine_v2("a cat", clip, "Unrated", "enh-out-test",
link_texts={"full_prompt": "a cat, soft light", "prompt": "a cat"}, **kw)
return refiner._v2_enhanced_output, clip
def test_enhanced_prompt_output_enhances_prompt_plus_postfix(tmp_path):
out, clip = _enhanced_output(tmp_path, prompt_enhance=True, prompt_enhance_output=True)
assert out == "a vivid cat on a sunlit sill"
assert any(t.endswith("a cat, soft light") for t, _ in clip.tokenize_calls)
def test_enhanced_prompt_output_falls_back_to_prompt_plus_postfix(tmp_path):
out, clip = _enhanced_output(tmp_path, prompt_enhance=False, prompt_enhance_output=True)
assert out == "a cat, soft light" and clip.generate_kwargs == {}
def test_enhanced_prompt_output_costs_nothing_when_unlinked(tmp_path):
out, clip = _enhanced_output(tmp_path, prompt_enhance=True)
assert out is None
assert not any(t.endswith("a cat, soft light") for t, _ in clip.tokenize_calls)