Close two more bindings the fingerprint could not see
Browse filesReview of 5298a45 found the object fix covered one of four shapes a node can
use to hand a configured callable to the model. Both gaps were verified against
the real _model_fingerprint before fixing, and both produced identical
fingerprints for different settings -- the silent-wrong-frames class the batch
exists to close.
A BOUND METHOD loses everything. vars() on one proxies to the underlying
function's __dict__, which is empty, not to the instance, so only __qualname__
survived. A node registering self.forward rather than self is the object bug one
attribute away.
A functools.partial loses everything too: no __name__, no __qualname__, no
__closure__, empty __dict__. It collapsed to the same "fn()" constant a bare
instance did, with its settings sitting untouched in func/args/keywords. That is
the SLA bug one binding away.
_callable_scalars now unwraps all four shapes -- closure, instance, bound
method, partial -- depth-bounded because a partial can wrap a partial.
Corrects a claim in DEVLOG 46. It said mutable containers being excluded keeps a
sampler's step counter out of the key. That is wrong: a counter is normally a
plain int on the instance, not a container, so it IS hashed, and a node that
counts on itself between queues stops the cache hitting for as long as it is
installed. The behaviour stands -- render twice rather than serve the wrong
frames once -- but it was an invisible cost. run() now prints the model
fingerprint beside the hop-cache line, because a cache that has quietly stopped
hitting is otherwise indistinguishable from one that is merely cold.
check_cache_keys.py gains six cases: bound method, partial keywords, partial
positional args, their agreement cases, and the mutable-public-scalar tradeoff
asserted explicitly rather than left implicit. Without that last one the
regression above passed the checker clean.
tools/notes.py listed pin_to_qwen under "anything chain-wide re-renders
everything". overlap and reference pictures were already stale there from
earlier commits. All three corrected and the Starter board regenerated.
13/13 checks pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013orqT1xNDezvdwv4f51MdW
- docs/DEVLOG.md +21 -6
- h3_ref_chain.py +50 -2
- tools/check_cache_keys.py +60 -3
- tools/notes.py +9 -4
- workflows/HandTieClips_Starter.json +3 -3
|
@@ -2342,12 +2342,27 @@ numbers -- a reuse threshold, a window, a step cap -- carried on the
|
|
| 2342 |
instance.
|
| 2343 |
|
| 2344 |
`_object_scalars` reads an object's public scalar attributes before falling
|
| 2345 |
-
back to its type name.
|
| 2346 |
-
|
| 2347 |
-
|
| 2348 |
-
|
| 2349 |
-
|
| 2350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2351 |
|
| 2352 |
The third is not a hole but a cost. `pin_to_qwen` sat in `chain_salt`, which
|
| 2353 |
mixes into every hop -- while `_attach_pin_to_qwen` is called only under `if
|
|
|
|
| 2342 |
instance.
|
| 2343 |
|
| 2344 |
`_object_scalars` reads an object's public scalar attributes before falling
|
| 2345 |
+
back to its type name. Review then found two more bindings that hide an
|
| 2346 |
+
object behind a callable and lose it just as completely. A BOUND METHOD:
|
| 2347 |
+
`vars()` on one proxies to the underlying function's `__dict__`, which is
|
| 2348 |
+
empty, not to the instance, so a node registering `self.forward` instead of
|
| 2349 |
+
`self` is the same bug one attribute away. And a `functools.partial`: no
|
| 2350 |
+
name, no cells, no attributes of its own, everything it carries sitting in
|
| 2351 |
+
`func`, `args` and `keywords` -- the SLA bug one binding away. Both
|
| 2352 |
+
collapsed to the same four characters as a bare instance did.
|
| 2353 |
+
`_callable_scalars` now unwraps all four shapes.
|
| 2354 |
+
|
| 2355 |
+
The tradeoff is worth writing down plainly, because the first draft of this
|
| 2356 |
+
entry got it wrong and review caught that too. A public scalar the node
|
| 2357 |
+
mutates during a run IS hashed. Only mutable *containers* are skipped, and a
|
| 2358 |
+
step counter is normally a plain int on the instance rather than a dict, so
|
| 2359 |
+
it is hashed like any other setting. A node that counts on itself between
|
| 2360 |
+
queues therefore moves the fingerprint between runs and the cache stops
|
| 2361 |
+
hitting for as long as it is installed. That is wasteful rather than wrong,
|
| 2362 |
+
and it is the direction this pack should err in -- but it is a real cost,
|
| 2363 |
+
and it used to be an invisible one. The run now prints the fingerprint
|
| 2364 |
+
beside the hop-cache line, because a cache that has quietly stopped hitting
|
| 2365 |
+
is otherwise indistinguishable from one that is merely cold.
|
| 2366 |
|
| 2367 |
The third is not a hole but a cost. `pin_to_qwen` sat in `chain_salt`, which
|
| 2368 |
mixes into every hop -- while `_attach_pin_to_qwen` is called only under `if
|
|
@@ -424,6 +424,47 @@ def _model_fingerprint(model):
|
|
| 424 |
and (isinstance(v, (str, int, float, bool)) or v is None)]
|
| 425 |
return "{" + ",".join(parts) + "}" if parts else ""
|
| 426 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
def _scalars(obj, depth=0):
|
| 428 |
"""Only names and scalars -- tensors and mutable state are not stable."""
|
| 429 |
if depth > 3:
|
|
@@ -440,7 +481,7 @@ def _model_fingerprint(model):
|
|
| 440 |
if isinstance(obj, (str, int, float, bool)) or obj is None:
|
| 441 |
return repr(obj)
|
| 442 |
if callable(obj):
|
| 443 |
-
return
|
| 444 |
return type(obj).__name__ + _object_scalars(obj)
|
| 445 |
|
| 446 |
h.update(_scalars(transformer).encode())
|
|
@@ -2084,8 +2125,15 @@ class HandTieClips:
|
|
| 2084 |
hop_store = _store.HopStore(
|
| 2085 |
os.path.join(folder_paths.get_temp_directory(), "h3_ref_chain_hops"),
|
| 2086 |
budget_gb=float(cache_budget_gb), fps=FPS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2087 |
print(f"[{TAG}] hop cache: {hop_store.root} "
|
| 2088 |
-
f"(budget {float(cache_budget_gb):.0f} GB
|
|
|
|
| 2089 |
# Note on how render_from works, since this is where the store appears:
|
| 2090 |
# the leading hops are NOT seeded into prev_imgs / prev_audio /
|
| 2091 |
# prev_sampled / prev_key from here. They run through the loop like any
|
|
|
|
| 424 |
and (isinstance(v, (str, int, float, bool)) or v is None)]
|
| 425 |
return "{" + ",".join(parts) + "}" if parts else ""
|
| 426 |
|
| 427 |
+
def _callable_scalars(fn, depth=0):
|
| 428 |
+
"""Settings a callable carries, whichever way it carries them.
|
| 429 |
+
|
| 430 |
+
There are four ways a node hands a configured callable to the model and
|
| 431 |
+
all four have to reach the hash, because they are interchangeable from
|
| 432 |
+
the installing node's point of view and indistinguishable from here:
|
| 433 |
+
|
| 434 |
+
* a closure -- cells (`_closure_scalars`)
|
| 435 |
+
* a configured instance -- its attributes (`_object_scalars`)
|
| 436 |
+
* a BOUND METHOD of a configured instance -- neither. `vars()` on a
|
| 437 |
+
bound method proxies to the underlying *function's* `__dict__`,
|
| 438 |
+
which is empty, so the instance's settings were invisible; only
|
| 439 |
+
`__qualname__` survived. A node registering `self.forward` rather
|
| 440 |
+
than `self` is the object case one attribute away.
|
| 441 |
+
* a `functools.partial` -- neither either. It has no `__name__`, no
|
| 442 |
+
`__qualname__`, no `__closure__`, and an empty `__dict__`, so it
|
| 443 |
+
collapsed to the constant "fn()" exactly as a bare instance did.
|
| 444 |
+
Everything it carries is in `func`, `args` and `keywords`.
|
| 445 |
+
|
| 446 |
+
Depth-bounded because `partial` can wrap `partial`.
|
| 447 |
+
"""
|
| 448 |
+
parts = [_closure_scalars(fn), _object_scalars(fn)]
|
| 449 |
+
if depth <= 3:
|
| 450 |
+
owner = getattr(fn, "__self__", None)
|
| 451 |
+
if owner is not None:
|
| 452 |
+
parts.append("@" + type(owner).__name__ + _object_scalars(owner))
|
| 453 |
+
inner = getattr(fn, "func", None)
|
| 454 |
+
if inner is not None and callable(inner):
|
| 455 |
+
bound = ["<" + _callable_scalars(inner, depth + 1)]
|
| 456 |
+
for a in (getattr(fn, "args", None) or ()):
|
| 457 |
+
bound.append(repr(a) if isinstance(a, (str, int, float, bool)) or a is None
|
| 458 |
+
else type(a).__name__)
|
| 459 |
+
kw = getattr(fn, "keywords", None) or {}
|
| 460 |
+
for k in sorted(kw, key=str):
|
| 461 |
+
v = kw[k]
|
| 462 |
+
bound.append(f"{k}=" + (repr(v)
|
| 463 |
+
if isinstance(v, (str, int, float, bool)) or v is None
|
| 464 |
+
else type(v).__name__))
|
| 465 |
+
parts.append(",".join(bound) + ">")
|
| 466 |
+
return "".join(parts)
|
| 467 |
+
|
| 468 |
def _scalars(obj, depth=0):
|
| 469 |
"""Only names and scalars -- tensors and mutable state are not stable."""
|
| 470 |
if depth > 3:
|
|
|
|
| 481 |
if isinstance(obj, (str, int, float, bool)) or obj is None:
|
| 482 |
return repr(obj)
|
| 483 |
if callable(obj):
|
| 484 |
+
return _callable_scalars(obj)
|
| 485 |
return type(obj).__name__ + _object_scalars(obj)
|
| 486 |
|
| 487 |
h.update(_scalars(transformer).encode())
|
|
|
|
| 2125 |
hop_store = _store.HopStore(
|
| 2126 |
os.path.join(folder_paths.get_temp_directory(), "h3_ref_chain_hops"),
|
| 2127 |
budget_gb=float(cache_budget_gb), fps=FPS)
|
| 2128 |
+
# The fingerprint is printed because it is the one cache input a
|
| 2129 |
+
# user cannot see and cannot derive. If a run that should have hit
|
| 2130 |
+
# re-rendered everything, this line moving between two runs says so
|
| 2131 |
+
# in one glance -- and a node that mutates a public scalar attribute
|
| 2132 |
+
# on itself between queues (see `_object_scalars`) is exactly the
|
| 2133 |
+
# case that would otherwise look like the cache is simply broken.
|
| 2134 |
print(f"[{TAG}] hop cache: {hop_store.root} "
|
| 2135 |
+
f"(budget {float(cache_budget_gb):.0f} GB, model {model_fp})",
|
| 2136 |
+
flush=True)
|
| 2137 |
# Note on how render_from works, since this is where the store appears:
|
| 2138 |
# the leading hops are NOT seeded into prev_imgs / prev_audio /
|
| 2139 |
# prev_sampled / prev_key from here. They run through the loop like any
|
|
@@ -30,6 +30,7 @@ to agree, because a fingerprint that always changes is a cache that never hits.
|
|
| 30 |
"""
|
| 31 |
from __future__ import annotations
|
| 32 |
|
|
|
|
| 33 |
import importlib.util
|
| 34 |
import os
|
| 35 |
import sys
|
|
@@ -147,7 +148,40 @@ def main():
|
|
| 147 |
!= fp(_Patcher(transformer=dit(_closure_configured(0.50)))),
|
| 148 |
"the SLA sparsity regression")
|
| 149 |
|
| 150 |
-
print("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
shared = {"calls": 0}
|
| 152 |
|
| 153 |
def _with_state():
|
|
@@ -156,9 +190,32 @@ def main():
|
|
| 156 |
return _override
|
| 157 |
before = fp(_Patcher(transformer=dit(_with_state())))
|
| 158 |
shared["calls"] += 17
|
| 159 |
-
ck("a counter
|
| 160 |
before == fp(_Patcher(transformer=dit(_with_state()))),
|
| 161 |
-
"hashing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
print()
|
| 164 |
if FAIL:
|
|
|
|
| 30 |
"""
|
| 31 |
from __future__ import annotations
|
| 32 |
|
| 33 |
+
import functools
|
| 34 |
import importlib.util
|
| 35 |
import os
|
| 36 |
import sys
|
|
|
|
| 148 |
!= fp(_Patcher(transformer=dit(_closure_configured(0.50)))),
|
| 149 |
"the SLA sparsity regression")
|
| 150 |
|
| 151 |
+
print("settings reached through a binding")
|
| 152 |
+
# A node hands the model `self.method` rather than `self`, or a
|
| 153 |
+
# functools.partial rather than a closure. Both are callable, both have an
|
| 154 |
+
# empty __dict__ of their own, and neither has cells -- so before they were
|
| 155 |
+
# unwrapped, both collapsed to a constant and every setting behind them was
|
| 156 |
+
# invisible. These are the object bug and the SLA bug in their third and
|
| 157 |
+
# fourth binding shapes.
|
| 158 |
+
class _Bound:
|
| 159 |
+
def __init__(self, reuse_threshold=0.05):
|
| 160 |
+
self.reuse_threshold = reuse_threshold
|
| 161 |
+
|
| 162 |
+
def patch(self, *a, **kw):
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
ck("a bound method carries its instance's settings",
|
| 166 |
+
fp(_Patcher(transformer=dit(_Bound(0.05).patch)))
|
| 167 |
+
!= fp(_Patcher(transformer=dit(_Bound(0.20).patch))),
|
| 168 |
+
"vars() on a bound method sees the function, not the instance")
|
| 169 |
+
ck("two identical bound methods agree",
|
| 170 |
+
fp(_Patcher(transformer=dit(_Bound(0.05).patch)))
|
| 171 |
+
== fp(_Patcher(transformer=dit(_Bound(0.05).patch))))
|
| 172 |
+
|
| 173 |
+
def _plain_override(*a, **kw):
|
| 174 |
+
return None
|
| 175 |
+
|
| 176 |
+
ck("a functools.partial carries its keywords",
|
| 177 |
+
fp(_Patcher(transformer=dit(functools.partial(_plain_override, sparsity=0.90))))
|
| 178 |
+
!= fp(_Patcher(transformer=dit(functools.partial(_plain_override, sparsity=0.50)))),
|
| 179 |
+
"no __name__, no cells, empty __dict__")
|
| 180 |
+
ck("a functools.partial carries its positional args",
|
| 181 |
+
fp(_Patcher(transformer=dit(functools.partial(_plain_override, 0.90))))
|
| 182 |
+
!= fp(_Patcher(transformer=dit(functools.partial(_plain_override, 0.50)))))
|
| 183 |
+
|
| 184 |
+
print("mutable run state")
|
| 185 |
shared = {"calls": 0}
|
| 186 |
|
| 187 |
def _with_state():
|
|
|
|
| 190 |
return _override
|
| 191 |
before = fp(_Patcher(transformer=dit(_with_state())))
|
| 192 |
shared["calls"] += 17
|
| 193 |
+
ck("a counter in a closure cell does not move the key",
|
| 194 |
before == fp(_Patcher(transformer=dit(_with_state()))),
|
| 195 |
+
"hashing a mutable container would miss the cache on every queue")
|
| 196 |
+
|
| 197 |
+
# The other half of that tradeoff, asserted rather than left implicit: a
|
| 198 |
+
# PUBLIC SCALAR attribute is hashed even when the node mutates it, so a
|
| 199 |
+
# node carrying a step counter on itself moves the fingerprint between runs
|
| 200 |
+
# and the cache stops hitting while it is installed. That is deliberate --
|
| 201 |
+
# this pack renders twice rather than serving the wrong frames once -- but
|
| 202 |
+
# it is a real cost, and the run log prints the fingerprint so it can be
|
| 203 |
+
# seen rather than guessed at. If this assertion ever flips, the tradeoff
|
| 204 |
+
# was changed and the docstring on `_object_scalars` needs to change with it.
|
| 205 |
+
class _Counting:
|
| 206 |
+
def __init__(self):
|
| 207 |
+
self.reuse_threshold = 0.05
|
| 208 |
+
self.cnt = 0
|
| 209 |
+
|
| 210 |
+
def __call__(self, *a, **kw):
|
| 211 |
+
return None
|
| 212 |
+
|
| 213 |
+
counting = _Counting()
|
| 214 |
+
seen = fp(_Patcher(transformer=dit(counting)))
|
| 215 |
+
counting.cnt += 7
|
| 216 |
+
ck("a public scalar counter DOES move the key (documented cost)",
|
| 217 |
+
seen != fp(_Patcher(transformer=dit(counting))),
|
| 218 |
+
"wasteful, not wrong -- see _object_scalars")
|
| 219 |
|
| 220 |
print()
|
| 221 |
if FAIL:
|
|
@@ -357,10 +357,15 @@ to 4 off disk. Hop 6 was rendered *from* hop 5, so it has to. **Edit the
|
|
| 357 |
earliest hop you dislike and work forward** -- that way each hop is paid for
|
| 358 |
once.
|
| 359 |
|
| 360 |
-
Anything chain-wide re-renders everything: resolution, aspect,
|
| 361 |
-
scheduler, either shift, `ref_image_size`,
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
Loved a hop? Put `"locked": true` and a stable `"id"` on that shot and it keeps
|
| 366 |
that exact take even when its inputs move. Unrelated to `subjects.N.locked`,
|
|
|
|
| 357 |
earliest hop you dislike and work forward** -- that way each hop is paid for
|
| 358 |
once.
|
| 359 |
|
| 360 |
+
Anything chain-wide re-renders everything: resolution, aspect, sampler,
|
| 361 |
+
scheduler, either shift, `ref_image_size`, the checkpoint, or the LoRA stack.
|
| 362 |
+
That is the usual reason the cache looks broken.
|
| 363 |
+
|
| 364 |
+
Some levers are cheaper than they look. `overlap`, `pin_to_qwen` and the pin
|
| 365 |
+
settings only reach hop 2 onward, so flipping one re-renders hops 2+ and leaves
|
| 366 |
+
hop 1 on disk. A reference picture is keyed on the hops it actually rides -- so
|
| 367 |
+
a re-crop counts even under the same filename, but swapping the file behind a
|
| 368 |
+
ref that only rides hop 5 leaves hops 1-4 alone.
|
| 369 |
|
| 370 |
Loved a hop? Put `"locked": true` and a stable `"id"` on that shot and it keeps
|
| 371 |
that exact take even when its inputs move. Unrelated to `subjects.N.locked`,
|
|
@@ -1261,7 +1261,7 @@
|
|
| 1261 |
"htc_card": "rules"
|
| 1262 |
},
|
| 1263 |
"widgets_values": [
|
| 1264 |
-
"## The rules that decide whether it works\n\nNot style preferences. This is how this model fails.\n\n### 1. The prompt is additive\n\nSampling runs at **cfg 1.0 with no negative branch**. Every concept you name is\nadded, and nothing can be removed by mentioning it -- `no cut` puts the word\n*cut* in front of the encoder. **Never write a negation.**\n\n### 2. Never name the thing you want to end\n\n\"The cook stops talking\" keeps her talking. Write the state you want as **a pose\nplus a sound**:\n\n> leans back against the counter with her lips closed, and lets her eyes move\n> slowly across the room. The kitchen is quiet apart from the low hum of the\n> refrigerator.\n\nAudio is always generated. Silence written as an absence comes back as speech,\nso **silence has to be written as a sound** -- room tone, a fridge, a single\nclick. Keep it narrowband: \"faint street noise\" renders as a five-second hiss.\n\n**The ban is on the idea, not on a word list.** *Fades, passes, wanes, subsides,\ndies down, eases off* all name an ending as surely as *stops* does, and all of\nthem add the thing they describe. Ask of each sentence: is this happening, or\nhas it finished happening?\n\n### 3. A state change belongs at the END of the previous shot\n\nEvery hop opens holding the frames it was handed, and the audio pin carries the\nprevious hop's tail across the join. Nothing you write in shot 3 can make shot 3\nstart quiet. **Arrive there before the previous shot ends.**\n\n### 4. A hop that ends on dialogue keeps talking\n\nSpeech at the end of hop N opens hop N+1 and propagates down the whole chain.\nLand each line **mid-hop** and leave a non-verbal action running into the seam --\nslicing, walking, a hand on a doorframe. Give every hop with no dialogue a sound\nbed of its own.\n\n
|
| 1265 |
],
|
| 1266 |
"color": "#432",
|
| 1267 |
"bgcolor": "#653"
|
|
@@ -1339,7 +1339,7 @@
|
|
| 1339 |
"htc_card": "trouble"
|
| 1340 |
},
|
| 1341 |
"widgets_values": [
|
| 1342 |
-
"## When it goes wrong\n\n| symptom | cause | fix |\n|---|---|---|\n| The clip cuts to the reference photo in its last seconds | The beat finished before the frames did | Set `tail`, give the beat enough to do |\n| A stray gesture or line in the closing second | `tail: ongoing` on the final shot | `settle` or `hold` |\n| She keeps talking after you asked for quiet | You named the ending | Pose plus a sound |\n| Dialogue continues into hops that have none written | The hop before ended on speech | Land the line early; non-verbal action into the seam; a sound bed on every quiet hop |\n| A character walks between two rooms and one morphs into the other | `continuous` across a location change | `match_cut` |\n|
|
| 1343 |
],
|
| 1344 |
"color": "#432",
|
| 1345 |
"bgcolor": "#653"
|
|
@@ -1391,7 +1391,7 @@
|
|
| 1391 |
"htc_card": "author"
|
| 1392 |
},
|
| 1393 |
"widgets_values": [
|
| 1394 |
-
"## Let a model write your plan\n\n
|
| 1395 |
],
|
| 1396 |
"color": "#432",
|
| 1397 |
"bgcolor": "#653"
|
|
|
|
| 1261 |
"htc_card": "rules"
|
| 1262 |
},
|
| 1263 |
"widgets_values": [
|
| 1264 |
+
"## The rules that decide whether it works\n\nNot style preferences. This is how this model fails.\n\n### 1. The prompt is additive\n\nSampling runs at **cfg 1.0 with no negative branch**. Every concept you name is\nadded, and nothing can be removed by mentioning it -- `no cut` puts the word\n*cut* in front of the encoder. **Never write a negation.**\n\n### 2. Never name the thing you want to end\n\n\"The cook stops talking\" keeps her talking. Write the state you want as **a pose\nplus a sound**:\n\n> leans back against the counter with her lips closed, and lets her eyes move\n> slowly across the room. The kitchen is quiet apart from the low hum of the\n> refrigerator.\n\nAudio is always generated. Silence written as an absence comes back as speech,\nso **silence has to be written as a sound** -- room tone, a fridge, a single\nclick. Keep it narrowband: \"faint street noise\" renders as a five-second hiss.\n\n**The ban is on the idea, not on a word list.** *Fades, passes, wanes, subsides,\ndies down, eases off* all name an ending as surely as *stops* does, and all of\nthem add the thing they describe. Ask of each sentence: is this happening, or\nhas it finished happening?\n\n### 3. A state change belongs at the END of the previous shot\n\nEvery hop opens holding the frames it was handed, and the audio pin carries the\nprevious hop's tail across the join. Nothing you write in shot 3 can make shot 3\nstart quiet. **Arrive there before the previous shot ends.**\n\n### 4. A hop that ends on dialogue keeps talking\n\nSpeech at the end of hop N opens hop N+1 and propagates down the whole chain.\nLand each line **mid-hop** and leave a non-verbal action running into the seam --\nslicing, walking, a hand on a doorframe. Give every hop with no dialogue a sound\nbed of its own.\n\n### 5. A walk between two rooms is `match_cut`\n\n`join: continuous` across a real location change makes the model morph one room\ninto the other mid-movement.\n\n### 6. Set `tail` on your last shot\n\nLeft at `ongoing`, the model is told action is still underway at the final frame\nand will invent something to satisfy it. Use `settle` or `hold`."
|
| 1265 |
],
|
| 1266 |
"color": "#432",
|
| 1267 |
"bgcolor": "#653"
|
|
|
|
| 1339 |
"htc_card": "trouble"
|
| 1340 |
},
|
| 1341 |
"widgets_values": [
|
| 1342 |
+
"## When it goes wrong\n\n| symptom | cause | fix |\n|---|---|---|\n| The clip cuts to the reference photo in its last seconds | The beat finished before the frames did | Set `tail`, give the beat enough to do |\n| A stray gesture or line in the closing second | `tail: ongoing` on the final shot | `settle` or `hold` |\n| She keeps talking after you asked for quiet | You named the ending | Pose plus a sound |\n| Dialogue continues into hops that have none written | The hop before ended on speech | Land the line early; non-verbal action into the seam; a sound bed on every quiet hop |\n| A character walks between two rooms and one morphs into the other | `continuous` across a location change | `match_cut` |\n| Silence renders as speech | Silence written as an absence | Name room tone, a fridge, a distant car |\n| Ambience is a five-second hiss | Broadband wording | Narrowband, or one discrete event |\n| Two characters' faces merge | Both declared as the same `subject` | One subject number per person |\n| The face becomes a different person partway through | A hop scheduled with no face plate. `locked` holds a face that is still right; only a plate rebuilds one that is gone, and the drift never self-corrects | Put the face ref on **every** hop |\n| A stylised plan renders photoreal | The node's hop-1 establishing line asserts live action | Name the medium in shot 1's first sentence, or clear the `establish` widget |\n| The film gets darker every hop | Each hop dims across its own frames and hands the darker tail on. Seam correction cannot see it | `tone_compensate=anchor`. `tone_anchor=0.35` removed ~60% of measured drift; 0.6 removes ~78% and costs ~0.5/255 more seam step. Also restate the light as a positive property in every beat |\n| A location introduced mid-plan drifts | It has no place plate of its own | Plate it, on the hop it arrives and every hop after |\n| A prop or garment changes colour or material | Named with no adjective, so each hop's encode is free to invent one | Repeat the adjective in every beat, and state it as a property in `context` |\n| A hard cut ~1.5 s into a hop, mid-scene | The previous hop over-delivered, so this beat instructs what its own live frame already did | One movement per hop; write the next beat so it is true from either ending |\n| A continuous join reads as a cut | Framing change with `camera: hold` | Earn it on the move, or `framing: keep` |\n| The run stops, naming a reference | That row's picture is not in `h3_refs` | Drop the file on the row, or clear its picture to run without it |\n| A reference has no effect on some hop | Its `shots` list leaves that hop out | List every hop the picture should ride |\n| A deliberately dark scene keeps being brightened | `anchor` cannot tell intent from drift | `\"tone\": \"rebase\"` on that scene's first shot |\n| You cannot tell which hop broke | 114 s is a lot to scrub | `contact_sheet=on` -- one row per hop, first and last frame |\n| A pasted plan is rejected as invalid JSON | Escaped double quotes mangled in transit | Single quotes around dialogue |\n\nEvery error message names the shot or the reference it came from. Nothing\nguesses."
|
| 1343 |
],
|
| 1344 |
"color": "#432",
|
| 1345 |
"bgcolor": "#653"
|
|
|
|
| 1391 |
"htc_card": "author"
|
| 1392 |
},
|
| 1393 |
"widgets_values": [
|
| 1394 |
+
"## Let a model write your plan\n\n`prompt_pack/` in the pack folder turns any chat model into a plan writer. In\nLM Studio, or anything with a system-prompt box:\n\n1. Load a model with **context 16384 or more**. The prompt is ~4,700 tokens and\n the reply another 1,000-2,000; a small window truncates the rules and you get\n invented directive names.\n2. Paste **`prompt_pack/SYSTEM_PROMPT.md`** into the **System Prompt** box.\n Nothing else goes in that box.\n3. **Temperature 0.3-0.5.** Higher and the JSON grows trailing commas and smart\n quotes.\n4. Describe the scene, and say how many hops and what pictures you have:\n\n > Six hops. A cook in a kitchen; she says one line, walks out into a hallway,\n > waits by a window, then comes back. I have a face photo, a photo of her\n > apron, and a photo of the kitchen.\n\n5. Each panel section has its own **JSON** disclosure at the bottom. The first\n ```json``` block goes in the one under **SCRIPT** (`shot_plan`), the second\n in the one under **REFERENCES** (`ref_plan`). Bad JSON keeps the last good\n version on screen and says so, rather than discarding your paste.\n6. **If the node rejects it, paste the error straight back into the chat.** One\n round trip usually fixes it.\n\nWant it to match a shape? Paste `prompt_pack/EXAMPLE_6_HOP.md` first.\n\nSmall models (7B-8B) hold the JSON schema but drift on the prose rules -- they\nwrite negations. Skim the beats before queueing.\n\n## Fixing one hop without re-rendering the rest\n\nSet **`cache_hops` to `on` before your first run.** It is off by default, and a\nhop that was never cached cannot be reused. Nothing to install.\n\nThe cache key **chains**, so editing shot 5 of 8 re-renders 5 to 8 and reuses 1\nto 4 off disk. Hop 6 was rendered *from* hop 5, so it has to. **Edit the\nearliest hop you dislike and work forward** -- that way each hop is paid for\nonce.\n\nAnything chain-wide re-renders everything: resolution, aspect, sampler,\nscheduler, either shift, `ref_image_size`, the checkpoint, or the LoRA stack.\nThat is the usual reason the cache looks broken.\n\nSome levers are cheaper than they look. `overlap`, `pin_to_qwen` and the pin\nsettings only reach hop 2 onward, so flipping one re-renders hops 2+ and leaves\nhop 1 on disk. A reference picture is keyed on the hops it actually rides -- so\na re-crop counts even under the same filename, but swapping the file behind a\nref that only rides hop 5 leaves hops 1-4 alone.\n\nLoved a hop? Put `\"locked\": true` and a stable `\"id\"` on that shot and it keeps\nthat exact take even when its inputs move. Unrelated to `subjects.N.locked`,\nwhich is identity text.\n\nFull detail in `PROMPTING.md`, under *Re-rolling one hop*."
|
| 1395 |
],
|
| 1396 |
"color": "#432",
|
| 1397 |
"bgcolor": "#653"
|