fix: camera noise declares the FreeNoise collision, matches the probe path, clamps pan; states the zoom-in cost
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
aec5ac4a50
commit
0bdf070fba
@@ -15,6 +15,11 @@ audio noise is never touched.
|
||||
All values are unit-free: pan is the fraction of the frame the noise travels over the clip
|
||||
(x positive = camera moves right, so the picture moves left; y positive = camera moves down),
|
||||
zoom is the end scale (above 1 = camera moves in), focus is where the zoom aims, 0..1.
|
||||
|
||||
Known cost: zooming in copies each source cell into several neighbours, so neighbouring
|
||||
values become alike (adjacent correlation ~0.34 at zoom 1.5, ~0.51 at 2.0 in the travelling
|
||||
part, roughly 0.2 to 0.3 after the default blend). Every value is still N(0, 1); whether the
|
||||
model minds that blockiness is a question for the GPU.
|
||||
"""
|
||||
|
||||
import math
|
||||
@@ -26,6 +31,12 @@ MODES = ("off", "manual")
|
||||
AMOUNT_MAX = 0.95
|
||||
|
||||
|
||||
def clamp_pan(v):
|
||||
"""A pan share in [-1, 1]; anything unreadable (NaN) is no pan."""
|
||||
v = float(v)
|
||||
return 0.0 if v != v else min(max(v, -1.0), 1.0)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Move:
|
||||
pan_x: float = 0.0
|
||||
|
||||
@@ -343,7 +343,7 @@
|
||||
{ name: "camera_pan_y", label: "Pan up / down", kind: "float", default: 0.0, min: -1.0, max: 1.0, step: 0.05, dependsOn: "camera_noise", dependsValue: "manual",
|
||||
hint: "Share of the frame height travelled over the clip. Positive: camera moves down." },
|
||||
{ name: "camera_zoom", label: "Zoom (in / out)", kind: "float", default: 1.0, min: 0.5, max: 2.0, step: 0.05, dependsOn: "camera_noise", dependsValue: "manual",
|
||||
hint: "Scale reached by the end. Above 1 the camera moves in, below 1 out." },
|
||||
hint: "Scale reached by the end. Above 1 the camera moves in, below 1 out. Zooming in stretches the noise, so keep it under 1.5 at first." },
|
||||
{ name: "camera_focus_x", label: "Zoom aims at (left to right)", kind: "float", default: 0.5, min: 0.0, max: 1.0, step: 0.05, dependsOn: "camera_noise", dependsValue: "manual",
|
||||
hint: "The point that stays put while everything else grows or shrinks around it." },
|
||||
{ name: "camera_focus_y", label: "Zoom aims at (top to bottom)", kind: "float", default: 0.5, min: 0.0, max: 1.0, step: 0.05, dependsOn: "camera_noise", dependsValue: "manual",
|
||||
|
||||
@@ -281,6 +281,7 @@ def test_the_new_features_lines_carry_their_state_colour():
|
||||
"shot memory: Active | reusing a liked shot of 5 (reuse 0.71 > fresh 0.40), amount 0.70",
|
||||
"shot memory: Active | fresh (reuse 0.30 < fresh 0.55)",
|
||||
"steering window: Active | 1 of 4 steps (peak gate 0.50)",
|
||||
"camera noise: Active | pan right 0.30, zoom in x1.30 toward (0.30, 0.50), 80% carried",
|
||||
"[FunPack] upscaled a.mp4 with 4x.safetensors: 96 frames -> funpack_upscaled/a_4x.mp4",
|
||||
)
|
||||
orange = (
|
||||
@@ -293,6 +294,8 @@ def test_the_new_features_lines_carry_their_state_colour():
|
||||
"decisiveness: Inactive | a 1-step schedule has no step before the output to carry an edit into",
|
||||
"shot memory: Inactive | left 1 sampling pass(es) alone, their latent already held a picture",
|
||||
"steering window: Inactive | nothing will steer on this schedule, every rating-driven mechanism is gated off",
|
||||
"camera noise: Inactive beyond the first context window | FreeNoise refills the later windows",
|
||||
"camera noise: Inactive | this pass starts from a picture (second pass, latent anchor or carried overlap)",
|
||||
)
|
||||
for line in green:
|
||||
text = line if line.startswith("[") else P + line
|
||||
|
||||
+9
-2
@@ -3349,7 +3349,7 @@ class FunPackLTXAVSceneChainSampler:
|
||||
}),
|
||||
"camera_zoom": ("FLOAT", {
|
||||
"default": 1.0, "min": 0.5, "max": 2.0, "step": 0.05,
|
||||
"tooltip": "Scale reached by the end of the clip. Above 1 = camera moves in, below 1 = out, 1 = none.",
|
||||
"tooltip": "Scale reached by the end of the clip. Above 1 = camera moves in, below 1 = out, 1 = none. Zooming in stretches the noise, which makes neighbouring values alike; keep it modest (under 1.5) until you have seen it work.",
|
||||
}),
|
||||
"camera_focus_x": ("FLOAT", {
|
||||
"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.05,
|
||||
@@ -5280,6 +5280,8 @@ class FunPackLTXAVSceneChainSampler:
|
||||
cand_noise = comfy.sample.prepare_noise(samples, cand_seed)
|
||||
if getattr(self, "_shot_memory", None) is not None:
|
||||
cand_noise = self._shot_memory.shape(cand_noise, samples, _raw_cond(positive))
|
||||
if getattr(self, "_camera_move", None) is not None:
|
||||
cand_noise, _ = _camera.shape(cand_noise, samples, self._camera_move, cand_seed)
|
||||
comfy.sample.sample_custom(
|
||||
model, cand_noise, float(cfg), sampler, probe_sigmas, positive, negative,
|
||||
samples, noise_mask=probe_latent.get("noise_mask"), seed=cand_seed,
|
||||
@@ -9777,11 +9779,16 @@ class FunPackLTXAVSceneChainSampler:
|
||||
self._camera_move, self._camera_said = None, set()
|
||||
if camera_noise != "off":
|
||||
self._camera_move = _camera.Move(
|
||||
pan_x=float(camera_pan_x), pan_y=float(camera_pan_y),
|
||||
pan_x=_camera.clamp_pan(camera_pan_x), pan_y=_camera.clamp_pan(camera_pan_y),
|
||||
zoom=min(max(float(camera_zoom), 0.5), 2.0),
|
||||
focus_x=min(max(float(camera_focus_x), 0.0), 1.0),
|
||||
focus_y=min(max(float(camera_focus_y), 0.0), 1.0),
|
||||
amount=float(camera_amount))
|
||||
if context_windows and context_window_freenoise:
|
||||
print("[FunPackSceneChain] camera noise: Inactive beyond the first context window | "
|
||||
"FreeNoise refills the later windows with shuffled earlier noise after this, "
|
||||
"so a scene longer than one window loses the move there; switch "
|
||||
"context_window_freenoise off to keep it")
|
||||
if (shot_memory != "off" or tsr == "learned" or late_guidance == "learned"
|
||||
or stas == "learned"):
|
||||
if not refinement_key_input:
|
||||
|
||||
@@ -113,3 +113,32 @@ def test_audio_noise_is_never_touched(monkeypatch):
|
||||
def test_describe_reads_back_the_move():
|
||||
m = cn.Move(pan_x=-0.3, zoom=1.5, focus_x=0.2, amount=0.8)
|
||||
assert m.describe() == "pan left 0.30, zoom in x1.50 toward (0.20, 0.50), 80% carried"
|
||||
|
||||
|
||||
def _adjacent_correlation(x):
|
||||
a, b = x[..., :-1], x[..., 1:]
|
||||
return float((a * b).mean() / (x.std() ** 2))
|
||||
|
||||
|
||||
def test_zoom_in_makes_neighbours_alike_but_pan_and_zoom_out_do_not():
|
||||
"""The cost is stated in the docstring and tooltip; this pins its size."""
|
||||
last = lambda m: _travel(m, t=5, h=48, w=84, c=8)[:, -1]
|
||||
assert abs(_adjacent_correlation(last(cn.Move(pan_x=0.5)))) < 0.05
|
||||
assert abs(_adjacent_correlation(last(cn.Move(zoom=0.5)))) < 0.05
|
||||
assert 0.2 < _adjacent_correlation(last(cn.Move(zoom=1.5))) < 0.5
|
||||
|
||||
|
||||
def test_pan_is_clamped_and_nan_is_no_pan():
|
||||
assert cn.clamp_pan(7) == 1.0 and cn.clamp_pan(-7) == -1.0
|
||||
assert cn.clamp_pan(float("nan")) == 0.0 and cn.clamp_pan(0.25) == 0.25
|
||||
|
||||
|
||||
def test_the_sampler_inputs_agree_with_the_editor_defaults():
|
||||
import samplers
|
||||
from movie_editor.backend import settings_card
|
||||
opt = samplers.FunPackLTXAVSceneChainSampler.INPUT_TYPES()["optional"]
|
||||
names = ["camera_noise", "camera_pan_x", "camera_pan_y", "camera_zoom",
|
||||
"camera_focus_x", "camera_focus_y", "camera_amount"]
|
||||
for name in names:
|
||||
assert opt[name][1]["default"] == settings_card._EDITOR_DEFAULTS[name], name
|
||||
assert opt["camera_noise"][1]["default"] == "off" and settings_card._EDITOR_DEFAULTS["camera_noise"] == "off"
|
||||
|
||||
Reference in New Issue
Block a user