File size: 9,225 Bytes
e488ce7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
"""Model profiles: the per-model clock facts the de-rope pipeline needs, so one
hold map (integer hold per world frame) can be retimed for any model.

A profile is five numbers and a flag:

  block        frames per latent time block. Holds of 2+ are quantised to a
               multiple of this, because a held frame only owns its own block
               when its hold fills whole blocks (LTX-2.5: 8; measured, d=8
               barely moves and d=16 does).
  hold_scale   how many world frames one oracle hold unit buys on this model.
               The oracle emits holds of 1-4 in H3 terms (scale 1). LTX-2.5
               needed x4 (hold 4 -> 16x) to move at all: MEASURED, not derived.
  legal        (step, offset): legal pixel lengths are step*k + offset
               (H3 17k+5, LTX-2.5 8k+1, Wan 4k+1).
  fps          the model's native frame rate (what a second of holds costs).
  cap_seconds  longest single pass, or None. LTX-2.5: 20 s, from its absolute-
               seconds positional cap. H3's wall is VRAM and moves with the card.
  measured     True only when hold_scale and block come from a ladder on this
               model. A custom or unmeasured profile is stamped as such in every
               report: a confident default here is how LTX d=8 fooled us for a day.

  latent       how that model's VIDEO LATENT maps onto pixel frames, so the jerk
               oracle can read the model's own latent: channels, and the token
               clock as (first, block): token 0 owns `first` frames, every later
               token owns `block` frames (LTX-2.5: 1 + 8k; Wan: 1 + 4k). H3 is
               the exception with its (1,4,4,4,4)-per-17 grid and five-phase
               normalisation, kept in motion.py bit-identical; `latent: None`
               here means "use the H3 code path".

Holds of 1 are never scaled: a hold of 1 is plain video on any model.

Presets live in PRESETS. Users extend them without touching code by dropping a
JSON object {id: profile} at <ComfyUI user dir>/mainodes_models.json (or the
path in $MAINODES_MODELS_JSON); a row there overrides a preset of the same id.
Pure python, no torch, no comfy: unit-testable.
"""
import json
import os

PRESETS = {
    "minimax-h3": dict(
        name="MiniMax-H3", block=1, hold_scale=1, legal=(17, 5), fps=24,
        cap_seconds=None, measured=True, latent=None,
        note="native: the oracle's holds are in H3 frames already; smear pads to 17k+5"),
    "ltx-2.5": dict(
        name="LTX-2.5", block=8, hold_scale=4, legal=(8, 1), fps=48,
        cap_seconds=20.0, measured=True, latent=dict(channels=128, first=1, block=8),
        note="time compressed x8; hot holds must fill whole 8-frame blocks. x4 measured 2026-08-21 "
             "(d=8 uniform: staircase 0.94-0.90 across the denoise range; d=16: 0.90-0.28). "
             "fps is the frame_rate the LTXV conditioning is told (48 in the deck's graphs); the 20 s "
             "positional cap is in seconds of THAT clock, so 913 dilated frames = 19.02 s. Longer "
             "clips are windows, spliced"),
    "wan-2.2 (unmeasured)": dict(
        name="Wan 2.2", block=4, hold_scale=4, legal=(4, 1), fps=16,
        cap_seconds=5.0, measured=False, latent=dict(channels=16, first=1, block=4),
        note="placeholder from the model's published grid (4x time, 4k+1 frames, 16 fps, "
             "trained at 81 frames = 5 s so that is the cap); hold_scale is a guess until a "
             "ladder is run. Treat every number as unmeasured"),
}

FIELDS = ("name", "block", "hold_scale", "legal", "fps", "cap_seconds", "measured", "note", "latent")


def _user_registry_path():
    p = os.environ.get("MAINODES_MODELS_JSON")
    if p:
        return p
    try:
        import folder_paths                      # only inside ComfyUI
        return os.path.join(folder_paths.get_user_directory(), "mainodes_models.json")
    except Exception:
        return None


def normalize(pid, raw):
    """Fill defaults and coerce types for a profile row; never raises on a
    missing optional field, always raises on a malformed required one."""
    p = dict(raw)
    out = dict(
        id=pid,
        name=str(p.get("name", pid)),
        block=max(1, int(p["block"])),
        hold_scale=max(1, int(p["hold_scale"])),
        legal=(int(p["legal"][0]), int(p["legal"][1])),
        fps=float(p.get("fps", 24)),
        cap_seconds=(None if p.get("cap_seconds") in (None, 0, "") else float(p["cap_seconds"])),
        measured=bool(p.get("measured", False)),
        note=str(p.get("note", "")),
        latent=(None if not p.get("latent") else dict(
            channels=int(p["latent"].get("channels", 0)),
            first=max(1, int(p["latent"].get("first", 1))),
            block=max(1, int(p["latent"].get("block", 1))))),
    )
    if out["legal"][0] < 1:
        raise ValueError(f"profile {pid}: legal step must be >= 1")
    return out


def load_profiles(extra_path=None):
    """PRESETS merged with the user registry (user rows win). Never raises:
    a broken registry prints and is skipped, so the pack still loads."""
    profiles = {k: normalize(k, v) for k, v in PRESETS.items()}
    path = extra_path or _user_registry_path()
    if path and os.path.exists(path):
        try:
            user = json.load(open(path))
            for k, v in user.items():
                profiles[str(k)] = normalize(str(k), v)
        except Exception as e:                   # a typo must not take the pack down
            print(f"[MAINodes] model registry {path} not loaded: {type(e).__name__}: {e}")
    return profiles


def legal_ceil(n, legal, min_k=0):
    """Smallest step*k + offset >= n."""
    step, off = legal
    k = max(min_k, -(-(n - off) // step))
    return step * k + off


def remap_holds(holds, profile):
    """Oracle holds (H3 units) -> holds on the target model's clock.

    1 stays 1. h >= 2 becomes h * hold_scale, quantised to a whole number of
    blocks (never below one block). H3's profile (block 1, scale 1) is the
    identity, so existing graphs are unchanged by construction."""
    b, s = profile["block"], profile["hold_scale"]
    out = []
    for h in holds:
        h = int(h)
        if h <= 1:
            out.append(1)
        else:
            out.append(max(b, int(round(h * s / b)) * b))
    return out


def pad_to_legal(holds, legal):
    """Extend the final hold so the total sits on the legal grid (what
    H3TimeSmear does with 17k+5). Returns (holds, total, pad)."""
    holds = list(holds)
    total = legal_ceil(sum(holds), legal)
    pad = total - sum(holds)
    holds[-1] += pad
    return holds, total, pad


def windows_needed(total_frames, profile):
    """How many passes a dilated length needs under the profile's cap."""
    if not profile.get("cap_seconds"):
        return 1
    cap = int(profile["cap_seconds"] * profile["fps"])
    return max(1, -(-total_frames // cap))


def report(holds_in, holds_out, total, pad, profile):
    held = sum(1 for h in holds_in if h > 1)
    n = len(holds_in)
    lines = [
        f"profile {profile['id']} ({profile['name']}): block {profile['block']}, "
        f"hold_scale x{profile['hold_scale']}, legal {profile['legal'][0]}k+{profile['legal'][1]}, "
        f"{profile['fps']:g} fps" + (f", cap {profile['cap_seconds']:g} s/pass" if profile.get('cap_seconds') else ""),
        f"world {n} frames, {held} held -> {total} dilated frames ({total / profile['fps']:.2f} s at "
        f"{profile['fps']:g} fps), tail pad {pad}",
        f"hold histogram in -> out: {_hist(holds_in)} -> {_hist(holds_out)}",
    ]
    w = windows_needed(total, profile)
    if w > 1:
        lines.append(f"EXCEEDS the per-pass cap: {w} windows needed (splice them)")
    if not profile.get("measured"):
        lines.append("UNMEASURED profile: block / hold_scale are not from a ladder on this model; "
                     "treat the retime as an experiment, not a recipe")
    return "\n".join(lines)


def _hist(holds):
    from collections import Counter
    c = Counter(int(h) for h in holds)
    return "{" + ", ".join(f"{k}:{v}" for k, v in sorted(c.items())) + "}"


# ------------------------------------------------------------ latent clock
# A generic (first, block) token clock: token 0 owns `first` pixel frames, each
# later token owns `block`. These are the three functions the oracle's planner
# needs; motion.py keeps H3's own (1,4,4,4,4)-per-17 versions and uses these
# only when a profile carries a `latent` entry.

class LatentClock:
    def __init__(self, profile):
        lat = profile["latent"]
        self.first, self.block = lat["first"], lat["block"]
        self.channels = lat.get("channels", 0)
        self.legal = profile["legal"]
        self.id = profile["id"]

    def tok_start_frame(self, t):
        return 0 if t == 0 else self.first + (t - 1) * self.block

    def frame_token(self, f, t_lat):
        if f < self.first:
            return 0
        return min(1 + (f - self.first) // self.block, t_lat - 1)

    def token_count(self, frames):
        n = legal_ceil(frames, self.legal)
        return 1 + max(0, -(-(n - self.first) // self.block))

    def legal_ceil(self, n):
        return legal_ceil(n, self.legal)