diff --git a/conditioning.py b/conditioning.py index b7a20078..2d4a6cdb 100644 --- a/conditioning.py +++ b/conditioning.py @@ -9920,7 +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, + prompt_enhance_output=False, prompt_enhance_scenes=True, _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, @@ -10571,11 +10571,17 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): # When the run splits into scenes, the per-scene conditionings REPLACE this base entry # — so enhancing the base too would be a second full generation whose result is then # thrown away. One generation per prompt that is actually encoded, never two. - _enhance_base = prompt_enhance and not split_by_transitions + # Off when nothing reads Studio's own conditioning (the Editor checks the wiring): + # rewriting a prompt whose encoding is discarded is a generation for nothing. + _enhance_own = prompt_enhance and prompt_enhance_scenes + if prompt_enhance and not prompt_enhance_scenes: + print("[FunPackVideoRefinerV2] Prompt enhancer: Studio's own prompt skipped — " + "nothing reads its conditioning; only the enhanced_prompt output is written.") + _enhance_base = _enhance_own and not split_by_transitions # top_k / min_p / penalties / greedy; `seed` 0 = follow the generation seed. _enhance_sampling = dict(prompt_enhance_sampling or {}) _enhance_seed = int(_enhance_sampling.pop("seed", 0) or 0) or seed - if prompt_enhance and not _enhance_base: + if _enhance_own and not _enhance_base: print("[FunPackVideoRefinerV2] Prompt enhancer: base prompt skipped — this run " "splits into scenes and each scene is enhanced instead.") if _enhance_base: @@ -10845,7 +10851,7 @@ class FunPackVideoRefinerV2(FunPackVideoRefiner): # authoritative on count, so rewriting the scenes as a single paragraph # and re-splitting could return a different number of scenes and desync # every clip from its anchor. One call per distinct text, cached. - if prompt_enhance and split_scene_texts: + if _enhance_own and split_scene_texts: _enhanced_texts = [] for _i, t in enumerate(split_scene_texts): _after, _st = self._v2_enhance_prompt( @@ -13845,6 +13851,7 @@ class FunPackStudio: 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_scenes=bool(rf.get("prompt_enhance_scenes", True)), 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)), diff --git a/movie_editor/backend/builder.py b/movie_editor/backend/builder.py index ed0b1273..4cb6acc1 100644 --- a/movie_editor/backend/builder.py +++ b/movie_editor/backend/builder.py @@ -1052,6 +1052,20 @@ def build(object_info: dict, models_config: dict, params: dict, media: dict | No _break_cycles(graph, report, protected_edges, _node_labels(slots, slot_node_id, object_info, core=CORE)) + # Nothing reads Studio's own conditioning (e.g. H3's reference-to-video node feeding the + # sampler directly): Studio's per-scene prompt enhancement would be thrown away. + if "studio" in graph and "sampler" in graph and \ + not _feeds_from(graph, graph["sampler"]["inputs"].get("positive"), ("studio", 1)): + try: + _ss_p = json.loads(str(graph["studio"]["inputs"].get("studio_settings") or "{}")) + except ValueError: + _ss_p = {} + if isinstance(_ss_p, dict): + _rf_p = _ss_p.get("refiner") if isinstance(_ss_p.get("refiner"), dict) else {} + _rf_p["prompt_enhance_scenes"] = False + _ss_p["refiner"] = _rf_p + graph["studio"]["inputs"]["studio_settings"] = json.dumps(_ss_p) + for msg in pipeline_wiring.validate_models_wiring(models_config): report["blocking"].append(msg) report["unsatisfied"].append(msg) @@ -1373,6 +1387,22 @@ def _reaches_upstream(graph, start, goal): return False +def _feeds_from(graph, value, source): + """Whether the link `value` is, or is built upstream from, output `source` (node, idx).""" + stack, seen = [value], set() + while stack: + v = stack.pop() + if not (isinstance(v, list) and len(v) == 2 and isinstance(v[0], str)): + continue + if (v[0], v[1]) == tuple(source): + return True + if v[0] in seen or v[0] not in graph: + continue + seen.add(v[0]) + stack.extend((graph[v[0]].get("inputs") or {}).values()) + return False + + def _edge_input(graph, dst, up): """The input name on `dst` that consumes `up`'s output (or None).""" for inp, val in (graph.get(dst, {}).get("inputs", {}) or {}).items(): diff --git a/movie_editor/tests/test_builder.py b/movie_editor/tests/test_builder.py index 27d92031..85f5cf00 100644 --- a/movie_editor/tests/test_builder.py +++ b/movie_editor/tests/test_builder.py @@ -1597,3 +1597,19 @@ def test_enhanced_prompt_link_wires_to_studios_output(): 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 + + +def test_studio_skips_its_own_enhancement_when_nothing_reads_its_conditioning(): + import json + oi = {**OI, "R2V": {"input": {"required": {"prompt": ["STRING", {"default": ""}]}}, + "output": ["CONDITIONING"], "output_name": ["CONDITIONING"]}} + + def rf_of(models): + graph, _ = builder.build(oi, models, PARAMS) + return json.loads(graph["studio"]["inputs"]["studio_settings"]).get("refiner", {}) + + assert "prompt_enhance_scenes" not in rf_of({"slots": []}) # default: Studio feeds it + bypassed = {"full_control": True, + "slots": [{"id": "r2v", "node_class": "R2V", "inputs": {}, "wires": {}}], + "core_overrides": {"sampler": {"positive": "out:r2v:CONDITIONING"}}} + assert rf_of(bypassed)["prompt_enhance_scenes"] is False diff --git a/tests/test_prompt_enhancer.py b/tests/test_prompt_enhancer.py index 3e8b8832..a88f860d 100644 --- a/tests/test_prompt_enhancer.py +++ b/tests/test_prompt_enhancer.py @@ -345,7 +345,7 @@ def _refine_source(): def test_the_base_prompt_is_not_enhanced_when_the_run_splits(): src = _refine_source() - assert "_enhance_base = prompt_enhance and not split_by_transitions" in src + assert "_enhance_base = _enhance_own and not split_by_transitions" in src def test_the_base_enhancement_is_gated_on_that_flag(): @@ -356,7 +356,7 @@ def test_the_base_enhancement_is_gated_on_that_flag(): def test_the_scene_path_is_still_enhanced_when_splitting(): src = _refine_source() - assert "if prompt_enhance and split_scene_texts:" in src + assert "if _enhance_own and split_scene_texts:" in src def test_the_skip_is_reported_not_silent(): diff --git a/tests/test_v2_category_alignment.py b/tests/test_v2_category_alignment.py index 48df95f3..e09028f8 100644 --- a/tests/test_v2_category_alignment.py +++ b/tests/test_v2_category_alignment.py @@ -2321,3 +2321,11 @@ 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) + + +def test_studio_skips_its_own_enhancement_when_told_nothing_reads_it(tmp_path): + out, clip = _enhanced_output(tmp_path, prompt_enhance=True, prompt_enhance_scenes=False) + assert out is None and clip.generate_kwargs == {} + out, clip = _enhanced_output(tmp_path, prompt_enhance=True, prompt_enhance_scenes=False, + prompt_enhance_output=True) + assert out == "a vivid cat on a sunlit sill"