Make the area cap a user knob, not a silent content judgment

The 35% area cap was refusing a user-requested 'hand' region (47% of
frame) with no way to override it — the code was unilaterally deciding
the request 'wasn't a detail pass' when the user never asked for that
judgment. Fair complaint: cost guardrails are not the same as content
decisions.

detail_max_area is now threaded end-to-end (detailing.area_cap param ->
sampler widget -> Editor knob in Chain Sampler > Experimental, 0.05-1.0,
default 0.35 unchanged). Raising it lets a region of any size through;
1.0 = no cap. The refusal note now points at the actual setting name
instead of citing a fixed constant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-07-19 01:10:27 +03:00
co-authored by Claude Fable 5
parent 30b27b734a
commit 0372ee8287
4 changed files with 46 additions and 9 deletions
+14 -7
View File
@@ -34,9 +34,10 @@ STAGE2_SIGMAS = (0.85, 0.725, 0.421875, 0.0)
# CLIPSeg heat above this (post-sigmoid) counts as "the named thing is here".
DEFAULT_THRESHOLD = 0.35
# Tubes larger than this fraction of the frame are refused: at that size the pass
# stops being region detailing and becomes a second full render (cost ~= 4x area
# fraction x tail steps), which belongs to a different feature.
# Default ceiling on tube area, as a fraction of the frame - purely a COST guardrail
# (cost ~= 4x area fraction x tail steps, so a big tube starts to rival a second full
# render) exposed as detail_max_area so the user decides the cost/quality trade-off,
# not this constant. Never silently override what the user asked to detail.
MAX_TUBE_AREA = 0.35
# Latent cells of context padding around the thresholded region, and the minimum
@@ -357,7 +358,8 @@ def _downscale_to(video, h, w):
def detail_refine_scene(chain, model, vae, sampler, positive, negative, latent,
detail_targets, upsampler, seed, cfg,
threshold=DEFAULT_THRESHOLD, strength=1.0, debug=False):
threshold=DEFAULT_THRESHOLD, strength=1.0, area_cap=MAX_TUBE_AREA,
debug=False):
"""Run the segmented-detail pass on one finished scene latent.
Returns (latent, note): the refined latent dict plus a run_mechanisms note.
@@ -368,6 +370,10 @@ def detail_refine_scene(chain, model, vae, sampler, positive, negative, latent,
nothing - always reported, with the max CLIPSeg score seen, because a
miss is otherwise indistinguishable from "nothing to detail here" when
it's actually just the threshold being wrong for this content.
`area_cap` is a COST guardrail, not a content judgment: the user named the
target, so a region crossing it is reported as a skip with the actual size
(never silently reinterpreted as "not found") and area_cap itself is a
user-facing knob (detail_max_area) - raise it, don't route around it here.
`chain` is the FunPackLTXAVSceneChainSampler instance (reuses _latent_tensors
and _sample_chunk so the refine denoise goes through the exact same path as
the scene it is polishing).
@@ -390,12 +396,13 @@ def detail_refine_scene(chain, model, vae, sampler, positive, negative, latent,
if heat is None:
return latent, f"segmented_detail(no match: '{target_str}' - no keyframe decoded)"
diag = {}
tube = find_tube(heat, lat_h, lat_w, threshold=threshold, debug=debug, diag=diag)
tube = find_tube(heat, lat_h, lat_w, threshold=threshold, area_cap=area_cap,
debug=debug, diag=diag)
if tube is None:
if diag.get("reason") == "area_cap":
note = (f"segmented_detail(no match: '{target_str}' region covers "
f"{diag['area']:.0%} of frame > {MAX_TUBE_AREA:.0%} cap - refused, "
"not a detail pass at that size)")
f"{diag['area']:.0%} of frame > {area_cap:.0%} cap - raise "
"detail_max_area if you want it detailed at this size anyway)")
else:
note = (f"segmented_detail(no match: '{target_str}', max CLIPSeg score "
f"{diag.get('max_heat', 0.0):.2f} < threshold {threshold:.2f} - "
+3 -1
View File
@@ -204,6 +204,8 @@
hint: "Blend of the refined region at paste-back. 1.0 = full replacement inside the silhouette; 0 disables the pass." },
{ name: "detail_threshold", label: "Detail match threshold", kind: "float", default: 0.35, min: 0.05, max: 0.9, step: 0.05, dependsOn: "segmented_detailing",
hint: "CLIPSeg match confidence required before a region counts as found. Its raw score for a real region is often well under 0.5 — if the scene report shows 'no match: max CLIPSeg score X < threshold', lower this toward X rather than assuming nothing is there." },
{ name: "detail_max_area", label: "Detail max area", kind: "float", default: 0.35, min: 0.05, max: 1.0, step: 0.05, dependsOn: "segmented_detailing",
hint: "Ceiling on how much of the frame the region may cover before it's refused, as a fraction of the frame. Cost-only guardrail (a bigger region costs more, roughly 4× its area × 3 steps) — never a judgment call about whether it's worth detailing. If the scene report shows a region refused at some %, raise this above that % to detail it anyway. 1.0 = no cap." },
];
const SAMPLER_KNOB_MAP = Object.fromEntries(SAMPLER_KNOBS.map((k) => [k.name, k]));
@@ -316,7 +318,7 @@
chain_timing: ["frame_overlap", "transition_duration", "use_same_seed"],
chain_guidance: ["cfg", "embed_guidance", "embed_guidance_source", "embed_guidance_strength", "score_slider", "score_slider_strength", "taste_nearest_prompt", "output_guidance", "output_guidance_strength", "dynashift", "dynashift_strength", "dynashift_threshold"],
chain_decode: ["decode_noise_scale", "decode_timestep", "decode_tile_size"],
chain_experimental: ["plateau_cache", "plateau_cache_threshold", "segmented_detailing", "detail_targets", "detail_strength", "detail_threshold", "mid_scene_guide", "mid_scene_guide_strength", "joyai_memory", "joyai_memory_size", "joyai_fix_frames", "joyai_frame_select", "joyai_memory_strength", "joyai_audio_memory", "v2a_grad_scale", "alg_blur_guides", "alg_guide_blur_strength", "alg_guide_blur_sigma_threshold", "bounded_attention_enabled", "identity_transfer_enabled", "source_id", "phase_scale", "id_strength", "arcface_mode", "debug_log"],
chain_experimental: ["plateau_cache", "plateau_cache_threshold", "segmented_detailing", "detail_targets", "detail_strength", "detail_threshold", "detail_max_area", "mid_scene_guide", "mid_scene_guide_strength", "joyai_memory", "joyai_memory_size", "joyai_fix_frames", "joyai_frame_select", "joyai_memory_strength", "joyai_audio_memory", "v2a_grad_scale", "alg_blur_guides", "alg_guide_blur_strength", "alg_guide_blur_sigma_threshold", "bounded_attention_enabled", "identity_transfer_enabled", "source_id", "phase_scale", "id_strength", "arcface_mode", "debug_log"],
};
function countChainView(p, id) {
+7 -1
View File
@@ -2470,6 +2470,10 @@ class FunPackLTXAVSceneChainSampler:
"default": 0.35, "min": 0.05, "max": 0.9, "step": 0.05,
"tooltip": "CLIPSeg match confidence (post-sigmoid) required before a region counts as found. CLIPSeg's raw score for a real, correctly-named region is often well under 0.5 — if the scene report shows 'no match: max CLIPSeg score X < threshold', lower this toward X (or just below it) rather than assuming nothing is there. Lower = more permissive (more false positives on unrelated regions); higher = stricter.",
}),
"detail_max_area": ("FLOAT", {
"default": 0.35, "min": 0.05, "max": 1.0, "step": 0.05,
"tooltip": "Ceiling on how much of the frame the detected region may cover before the pass refuses it, as a fraction of frame area. This is a COST guardrail only (cost ~= 4x area x 3 steps, so a large region starts to rival a second full render), never a judgment about whether the region is worth detailing — if the scene report shows a region refused at some %, raise this above that % to detail it anyway (up to 1.0 = no cap, full-frame allowed).",
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
@@ -4670,6 +4674,7 @@ class FunPackLTXAVSceneChainSampler:
taste_nearest_prompt=False,
segmented_detailing=False, detail_targets="hands",
detail_upsampler="None", detail_strength=1.0, detail_threshold=0.35,
detail_max_area=0.35,
unique_id=None, prompt=None):
if not isinstance(positive, list) or not positive:
raise ValueError("positive conditioning must contain at least one scene entry.")
@@ -5137,7 +5142,8 @@ class FunPackLTXAVSceneChainSampler:
sampled, _detail_note = _detailing.detail_refine_scene(
self, model, vae, sampler, scene_positive, scene_negative, sampled,
detail_targets, _detail_upsampler_model, scene_seed, cfg,
threshold=detail_threshold, strength=detail_strength, debug=debug_log)
threshold=detail_threshold, strength=detail_strength,
area_cap=detail_max_area, debug=debug_log)
# detail_refine_scene always returns a diagnostic note when it actually
# ran detection (miss or hit) — None only means it never attempted
# detection at all (which can't happen on this branch).
+22
View File
@@ -299,6 +299,28 @@ def test_refine_reports_area_cap_reason(monkeypatch):
assert "cap" in note.lower()
assert chain.chunk_calls == []
def test_refine_area_cap_is_a_cost_knob_not_a_judgment(monkeypatch):
"""The cap must never override what the user explicitly asked to detail — it's a
cost guardrail the user can raise, not FunPack deciding a big region 'isn't worth
detailing'. Same whole-frame region: refused at the default cap, allowed once the
user raises detail_max_area (mirrors area_cap, threaded from the sampler node)."""
chain = _FakeChain()
latent, video = _scene_latent()
monkeypatch.setattr(detailing, "_decode_detection_frames",
lambda *a, **k: [(0, torch.zeros(160, 240, 3))])
monkeypatch.setattr(detailing, "_clipseg_heat", lambda *a, **k: torch.full((160, 240), 0.9))
monkeypatch.setattr(
detailing, "_run_upsampler",
lambda upsampler, crop, vae, debug=False: torch.nn.functional.interpolate(
crop, scale_factor=(1, 2, 2), mode="nearest"))
out_default, note_default = detailing.detail_refine_scene(
chain, "m", "v", "s", [], [], latent, "hands", object(), 0, 1.0)
assert out_default is latent and "cap" in note_default.lower()
out_raised, note_raised = detailing.detail_refine_scene(
chain, "m", "v", "s", [], [], latent, "hands", object(), 0, 1.0, area_cap=1.0)
assert note_raised is not None and "segmented_detail(" in note_raised and "cap" not in note_raised.lower()
assert out_raised is not latent
def _patch_detection_and_upsampler(monkeypatch):
"""Hot square in pixel space; upsampler = exact 2x nearest (model-free)."""
heat = torch.zeros(160, 240)