diff --git a/CHANGELOG.md b/CHANGELOG.md index bebbfc3d..364e455a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,8 @@ guidance no longer edit the last step.** On few-step schedules the last step is the video, and edits there showed as grain. Each edit now goes into the next step's input. +- Decisiveness says when a schedule leaves it too little to act on (simple at 4 steps) and then + does not learn from that run's rating. New features' log lines now colour by state. - The harmless `set_mempolicy: Operation not permitted` line from ffmpeg is hidden (said once). ## [4.0.0] "Blinding Blackout" - 2026-08-27 diff --git a/movie_editor/backend/bridge.py b/movie_editor/backend/bridge.py index ea365cf6..8ad775b2 100644 --- a/movie_editor/backend/bridge.py +++ b/movie_editor/backend/bridge.py @@ -231,9 +231,9 @@ _ERROR = _re.compile(r"\b(fail(s|ed|ing|ures?)?|\w*errors?|\w*exceptions?|traceb r"refused|out of memory|nan|critical|fatal)\b", _re.I) _WARN = _re.compile(r"\b(warn(s|ing|ings)?|skip(s|ped|ping)?|inactive|disabled|not ready|could not|" r"cannot|can't|missing|ignor(ed|ing)|fallback|falling back|stripped|leaked|" - r"deprecated|no module named)\b", _re.I) + r"deprecated|no module named|inert|barely|nothing (will )?steer(ed)?)\b", _re.I) _OK = _re.compile(r"\b(active|done|saved|recorded|updated|loaded|finished|completed?|success(ful|fully)?|" - r"succeeded|prompt executed|installed|applied)\b", _re.I) + r"succeeded|prompt executed|installed|applied|upscaled)\b", _re.I) # White (info) is the terminal's own colour, so it gets no code. _TERM = {"error": "\x1b[91m", "warn": "\x1b[38;5;208m", "ok": "\x1b[92m"} diff --git a/movie_editor/tests/test_bridge.py b/movie_editor/tests/test_bridge.py index f6e2c9b9..54975774 100644 --- a/movie_editor/tests/test_bridge.py +++ b/movie_editor/tests/test_bridge.py @@ -266,3 +266,38 @@ def test_a_traceback_is_red_through_its_closing_line(): lines = ["[FunPack][movie] x failed:", "Traceback (most recent call last):", ' File "a.py", line 1, in f', "KeyboardInterrupt", "next unrelated line"] assert bridge.log_levels(lines) == ["error", "error", "error", "error", "info"] + + +def test_the_new_features_lines_carry_their_state_colour(): + """Green = it is acting, orange = it cannot or barely does, red = it failed.""" + P = "[FunPackSceneChain] " + green = ( + "STAS: Active | alpha 2.00 at block 15: steered 12 call(s), channels 3 and 41", + "STAS: Active | alpha learned 2.00 from 4 rating(s), trying 2.10", + "late-branch guidance: Active | strength 0.50, a copy with block 43 left out, sharing " + "blocks 0-42 (~14% extra per step, none on the last), edits the picture (the sound can react).", + "late-branch guidance: Active | guided 3 step call(s)", + "decisiveness: Active | learned k 1.020 from 7 rating(s), trying 1.050, push up to 9% of a step's noise", + "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)", + "[FunPack] upscaled a.mp4 with 4x.safetensors: 96 frames -> funpack_upscaled/a_4x.mp4", + ) + orange = ( + "STAS: Inactive | no channel at block 15 is over 50x the mean, nothing steered -- try another block", + "STAS: Inactive | H3 only", + "late-branch guidance: Inactive | no step call reached it, nothing guided", + "late-branch guidance: Inactive | did not apply this run, so this run's rating will not teach its learned strength", + "decisiveness: barely acts on this schedule (its push is at most 1.9% of a step's noise, the " + "last step is never edited), so this run's rating will not teach k | learned k 0.900", + "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", + ) + for line in green: + text = line if line.startswith("[") else P + line + assert bridge.log_level(text) == "ok", text + for line in orange: + text = line if line.startswith("[") else P + line + assert bridge.log_level(text) == "warn", text + assert bridge.log_level(P + "decisiveness failed (boom), passing through") == "error" diff --git a/samplers.py b/samplers.py index abc8a942..896359ff 100644 --- a/samplers.py +++ b/samplers.py @@ -941,8 +941,8 @@ class _InputSteer: where = self._where(args) if where is not None and where[1] <= 1: _log.note_once("FunPackSceneChain", - f"{self.what}: a 1-step schedule has no step before the output " - f"to carry an edit into, so it does nothing this run", + f"{self.what}: Inactive | a 1-step schedule has no step before the " + f"output to carry an edit into, so it does nothing this run", key=f"input steer 1 step:{self.what}") return where is not None and where[0] >= where[1] - 1 @@ -4297,12 +4297,12 @@ class FunPackLTXAVSceneChainSampler: if _stas_report is not None: _line, _n = _stas_report() self._stas_ran = getattr(self, "_stas_ran", 0) + _n - print(f"[FunPackSceneChain] STAS: {_line}") + print(f"[FunPackSceneChain] STAS: {'Active' if _n else 'Inactive'} | {_line}") if _late_stats is not None: self._late_ran = getattr(self, "_late_ran", 0) + _late_stats["guided"] print("[FunPackSceneChain] late-branch guidance: " + ( - f"guided {_late_stats['guided']} step call(s)" if _late_stats["guided"] - else (_late_stats["why"] or "no step call reached it, nothing guided"))) + f"Active | guided {_late_stats['guided']} step call(s)" if _late_stats["guided"] + else "Inactive | " + (_late_stats["why"] or "no step call reached it, nothing guided"))) if _phrase_probe is not None: self._report_phrase_probe(_phrase_probe[1]) # The masked probe passes ran through every capture hook too, so this @@ -8688,7 +8688,7 @@ class FunPackLTXAVSceneChainSampler: return (stats["why"] or f"no channel at block {block} is over " f"{_stas.MA_RATIO:.0f}x the mean, nothing steered -- try another block"), 0 - print(f"[FunPackSceneChain] STAS: alpha {_alpha:.2f} at block {block}, first " + print(f"[FunPackSceneChain] STAS: Active | alpha {_alpha:.2f} at block {block}, first " f"{_stas.EARLY_FRACTION:.0%} of steps, frame 0 + frame edges, picture rows (the sound can react).") return patched, _report except Exception as _e: # noqa: BLE001 @@ -8808,8 +8808,8 @@ class FunPackLTXAVSceneChainSampler: patched.model_options["model_function_wrapper"] = _tag_scene_wrapper( _late_wrapper, old_wrapper) - print(f"[FunPackSceneChain] late-branch guidance: strength {_w:.2f}, weak copy " - f"skips block {branch} and shares 0-{branch - 1} " + print(f"[FunPackSceneChain] late-branch guidance: Active | strength {_w:.2f}, " + f"a copy with block {branch} left out, sharing blocks 0-{branch - 1} " f"(~{100.0 * (len(dit) - branch) / len(dit):.0f}% extra per step, none on the last), " f"edits the picture (the sound can react).") return patched, stats @@ -9736,36 +9736,49 @@ class FunPackLTXAVSceneChainSampler: self._late_w, self._late_block = 0.0, int(late_guidance_block) self._stas_alpha, self._stas_block = None, int(stas_block) self._late_ran, self._stas_ran = 0, 0 # calls the features actually changed + self._tsr_acts = True # False: too weak on this schedule to be rated if (shot_memory != "off" or tsr == "learned" or late_guidance == "learned" or stas == "learned"): if not refinement_key_input: print("[FunPackSceneChain] shot memory / learned decisiveness / learned " - "late-branch guidance / learned STAS: needs refinement_key_input -- " - "off this run") + "late-branch guidance / learned STAS: Inactive | needs refinement_key_input") if shot_memory != "off" and refinement_key_input: self._shot_memory = _shot_memory.ShotMemory( refinement_key_input, shot_memory, shot_memory_amount) if tsr != "off": self._tsr_k, _tsr_note = _tsr.choose_k(refinement_key_input, tsr, tsr_k) if _tsr_note: - print(f"[FunPackSceneChain] decisiveness: {_tsr_note}") + _reach = None + if self._is_h3 and self._tsr_k != 1.0: + try: + _reach = _tsr.reach(sigmas.flatten().tolist(), self._tsr_k) + except Exception: # noqa: BLE001 + _reach = None + if _reach is not None and _reach < _tsr.INERT_SHARE: + self._tsr_acts = False + print(f"[FunPackSceneChain] decisiveness: barely acts on this schedule (its " + f"push is at most {_reach:.1%} of a step's noise, the last step is never " + f"edited), so this run's rating will not teach k | {_tsr_note}") + else: + print(f"[FunPackSceneChain] decisiveness: Active | {_tsr_note}" + + (f", push up to {_reach:.0%} of a step's noise" if _reach is not None else "")) if late_guidance != "off": if not self._is_h3: - print("[FunPackSceneChain] late-branch guidance: H3 only -- off this run") + print("[FunPackSceneChain] late-branch guidance: Inactive | H3 only") else: _w, _late_note = _late.DIAL.choose( refinement_key_input, late_guidance, late_guidance_strength) self._late_w = max(0.0, _w or 0.0) if _late_note: - print(f"[FunPackSceneChain] late-branch guidance: {_late_note}") + print(f"[FunPackSceneChain] late-branch guidance: Active | {_late_note}") if stas != "off": if not self._is_h3: - print("[FunPackSceneChain] STAS: H3 only -- off this run") + print("[FunPackSceneChain] STAS: Inactive | H3 only") else: self._stas_alpha, _stas_note = _stas.DIAL.choose( refinement_key_input, stas, stas_alpha) if _stas_note: - print(f"[FunPackSceneChain] STAS alpha: {_stas_note}") + print(f"[FunPackSceneChain] STAS: Active | alpha {_stas_note}") # Per-bucket value functions for trajectory_guidance. Separate from the one above # because they answer a different question: that one asks "is this a good finish", @@ -9808,13 +9821,13 @@ class FunPackLTXAVSceneChainSampler: _cov = _steer_ramp_coverage(_steer_ramp, sigmas) if _cov is not None: _gated, _total, _peak = _cov - print(f"[FunPackSceneChain] steering window: {_gated} of {_total} steps " - f"(peak gate {_peak:.2f})" + print(f"[FunPackSceneChain] steering window: {'Active' if _gated else 'Inactive'} | " + f"{_gated} of {_total} steps (peak gate {_peak:.2f})" + (" — read off the schedule's base grid; each edit goes into the next " "step's input, the last step is the model's own" if self._is_h3 else "")) if _gated == 0: - print("[FunPackSceneChain] steering window: NOTHING will steer on this " - "schedule — every rating-driven mechanism is gated off") + print("[FunPackSceneChain] steering window: Inactive | nothing will steer on " + "this schedule, every rating-driven mechanism is gated off") # Trajectory probe (FUNPACK_TRAJECTORY_PROBE=1): record the predicted x0 per # schedule bucket so a later analysis can say whether ratings separate EARLY, in the @@ -11008,16 +11021,17 @@ class FunPackLTXAVSceneChainSampler: # of THIS run would score the shots / k of an older, unrated one. if self._shot_memory is not None: for _note in self._shot_memory.notes: - print(f"[FunPackSceneChain] shot memory: {_note}") + print(f"[FunPackSceneChain] shot memory: " + f"{'Inactive' if 'left alone' in _note else 'Active'} | {_note}") if self._shot_memory.skipped: - print(f"[FunPackSceneChain] shot memory: left {self._shot_memory.skipped} " - f"sampling pass(es) alone -- their latent already held a picture " + print(f"[FunPackSceneChain] shot memory: Inactive | left {self._shot_memory.skipped} " + f"sampling pass(es) alone, their latent already held a picture " f"(second pass, latent anchor or carried overlap), so the noise " f"does not decide the shot there") self._shot_memory.save_pending() else: _shot_memory.clear_pending(refinement_key_input) - if tsr == "learned": + if tsr == "learned" and getattr(self, "_tsr_acts", True): _tsr.save_pending(refinement_key_input, self._tsr_k) else: _tsr.clear_pending(refinement_key_input) @@ -11032,7 +11046,7 @@ class FunPackLTXAVSceneChainSampler: continue _dial.clear_pending(refinement_key_input) if _on and self._is_h3: - print(f"[FunPackSceneChain] {_name}: did not apply this run, so this " + print(f"[FunPackSceneChain] {_name}: Inactive | did not apply this run, so this " f"run's rating will not teach its learned strength") # Trajectory probe: same run/rating pairing as the two above — the rating that scores diff --git a/tests/test_input_steer.py b/tests/test_input_steer.py index 5fd37539..3a16941e 100644 --- a/tests/test_input_steer.py +++ b/tests/test_input_steer.py @@ -201,3 +201,12 @@ def test_h3_a_one_step_schedule_says_it_does_nothing(capsys): _euler(model.model_options["model_function_wrapper"], torch.randn(1, 2, 3, 4, 4), schedule=torch.tensor([1.0, 0.0])) assert "1-step schedule" in capsys.readouterr().out + + +def test_decisiveness_reach_tells_a_weak_schedule_from_a_strong_one(): + simple4 = [1.0, 0.973, 0.923, 0.8, 0.0] + beta57_8 = [1.0, 0.99, 0.969, 0.933, 0.872, 0.764, 0.563, 0.235, 0.0] + assert tsr.reach(simple4, 0.9) < tsr.INERT_SHARE # ~2%: can't be felt + assert tsr.reach(beta57_8, 0.95) > tsr.INERT_SHARE * 3 # a real push + assert tsr.reach([1.0, 0.0], 0.9) == 0.0 # one step: nothing to carry into + assert tsr.reach(simple4, 1.0) == 0.0 # k = 1 is exactly off diff --git a/tsr.py b/tsr.py index 718e7934..8cc40210 100644 --- a/tsr.py +++ b/tsr.py @@ -44,6 +44,26 @@ def rescale_x0(x, x0, sigma, k): return (x - float(sigma) * r * eps) / a +# Below this share of a step's input noise, a push cannot be felt (simple@4, k 0.9 is ~2%). +# ponytail: judged by eye on one schedule, not measured; tune it if a rental says otherwise. +INERT_SHARE = 0.03 + + +def reach(sigmas, k, s=S): + """The largest share of a step's input noise that decisiveness's push makes up when each + push rides into the NEXT step's input (H3): sigma*|1-r|/(1-sigma), scaled by the landing + step's (1 - sigma) and set against its noise sigma. The last step is never edited.""" + vals = [float(v) for v in sigmas] + best = 0.0 + for i in range(len(vals) - 2): + s0, s1 = vals[i], vals[i + 1] + if s0 < 1e-4 or s1 < 1e-4 or 1.0 - s0 < 1e-3: + continue + push = s0 * abs(1.0 - factor(s0, k, s)) / (1.0 - s0) * (1.0 - s1) + best = max(best, push / s1) + return best + + def _path(refinement_key): try: from .conditioning import refinement_state_path