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:
co-authored by
Claude Fable 5
parent
30b27b734a
commit
0372ee8287
+14
-7
@@ -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} - "
|
||||
|
||||
@@ -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
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user