video: a model says how long its clips can be (v0.3.4)

Township's clip lengths were tuned when the only video model could hold ~81
frames in one call, so a 70-second match became a dozen short cuts. LongCat was
pretrained on continuation and the server renders a long shot in segments, so
the same match wants a handful of long takes — and the frame numbers that suit
one are wrong for the other by a factor of four. Only the two starter templates
knew that, and only because someone typed it in.

Worse, nothing snapped a picked count onto what the model can emit:
random.randint handed the server lengths like 250, which is not 4n+1 for any
VAE and not on LongCat's 93+80k segment grid, so the server rounded and the
plan's own numbers became fiction. On LongCat that also silently costs
block-sparse attention, because every point of 93+80k is a 16n+13 count BSA
can tile and nothing between them is.

Server — codai/models/video_geometry.py is the geometry once: native rate, the
legal frame grid, how much ONE render holds, whether continuation is native,
and the side multiple below which the fast attention path is lost. Published
per model on /v1/models (ModelInfo.video) and used by the render path itself,
so what the server advertises and what it rounds to cannot drift. The numbers
were spread across three modules that could not tell a client; the literals
here are pinned to each source by tests, since the LongCat and H3 workers run
in 3.10 venvs this cannot import. models.json overrides every field.

Township — the five frame fields now default to 0 = AUTO: derive the budget
from the model's geometry and this tool's seconds band, then snap both ends and
every pick onto the model's grid. Seconds, not frames, because clip count falls
out of long_target / (frames/fps). An explicit number still wins, so a saved
config keeps exactly the budget it had. Entrances and the face-off get their own
shorter band instead of borrowing the fight budget (on LongCat that made every
entrance an 11-22s take). Outcome shares are snapped per shot and the total
rewritten, the per-render chunk comes from the model (Wan keeps its
conservative 50), and the chaining decision reads the published continuation
rather than matching the model's name.

At the defaults a 70s match goes from 9-11 Wan cuts to 4-6 LongCat takes, each
one request with native continuation, with BSA still on.

1607 tests pass; the 17 failures and the test_broker_protocol hang are
pre-existing and unrelated (verified with the diff stashed).
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent 69240b20
...@@ -141,6 +141,48 @@ Response shape: ...@@ -141,6 +141,48 @@ Response shape:
} }
``` ```
A **video** model carries one extra object, `video`, describing what it can actually
emit — so a client can plan around the model instead of guessing from its name:
```json
{
"id": "longcat",
"type": "video",
"capabilities": ["video_generation"],
"video": {
"family": "longcat",
"native_fps": 15,
"frame_base": 93,
"frame_step": 80,
"min_frames": 93,
"max_frames": null,
"max_frames_per_render": 93,
"segment_frames": 93,
"cond_frames": 13,
"side_multiple": 64,
"continuation": "native"
}
}
```
| Field | Meaning |
|---|---|
| `native_fps` | The rate the model was trained to produce |
| `frame_base`, `frame_step` | Legal TOTAL frame counts are `frame_base + frame_step * k`. Wan is `1 + 4k` (its VAE's 4n+1), LongCat `93 + 80k` (one 93-frame segment, each continuation re-rendering 13 of them and so adding 80), MiniMax-H3 `5 + 17k` |
| `min_frames`, `max_frames` | Limits on the total; `null` = none (LongCat's segment loop runs as long as asked) |
| `max_frames_per_render` | How much ONE model call holds. Wan is ~81; past that the frames visibly jump, so longer clips must be chained |
| `segment_frames`, `cond_frames` | For a natively-continuing model: the segment length and how much of it re-renders the previous tail |
| `side_multiple` | Width and height should be multiples of this, or the model falls back to a slower attention path (LongCat at 480 silently loses block-sparse attention) |
| `continuation` | `native` (the server generates a long shot in segments — ask for the whole length in one request), `chained` (the client extends from a frame tail), `none` |
A count off the grid is **rounded by the server**, so a request for 250 LongCat frames
comes back a different length than it asked for. Snap client-side and the plan and the
output agree. Per-model overrides in `models.json`
(`video_native_fps`, `video_frame_base`, `video_frame_step`, `video_min_frames`,
`video_max_frames`, `video_max_frames_per_render`, `video_side_multiple`,
`video_segment_frames`, `video_cond_frames`) win over the built-in family defaults, so a
checkpoint the server has never seen before is configurable rather than mis-described.
Example: Example:
```bash ```bash
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.3.3" __version__ = "0.3.4"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -2158,7 +2158,16 @@ def _sync_video_loras(pipe, loras) -> None: ...@@ -2158,7 +2158,16 @@ def _sync_video_loras(pipe, loras) -> None:
def _snap_wan_frames(n: int) -> int: def _snap_wan_frames(n: int) -> int:
"""Snap a frame count to the Wan VAE's temporal grid (4k+1). The VACE """Snap a frame count to the Wan VAE's temporal grid (4k+1). The VACE
video/mask lists must be exactly num_frames long, and the pipeline expects a video/mask lists must be exactly num_frames long, and the pipeline expects a
4k+1 count, so we round to the nearest valid value (min 5).""" 4k+1 count, so we round to the nearest valid value (min 5).
Delegates to codai/models/video_geometry.py, which is the same grid /v1/models
publishes to clients — so what the server snaps to and what it advertises cannot
drift apart. The arithmetic is kept inline as a fallback because this is on the
render path and must never fail over an import."""
try:
from codai.models import video_geometry
return video_geometry.snap_frames(n, video_geometry.FAMILIES["wan"])
except Exception:
n = max(5, int(n)) n = max(5, int(n))
k = round((n - 1) / 4) k = round((n - 1) / 4)
return max(5, k * 4 + 1) return max(5, k * 4 + 1)
......
...@@ -5666,6 +5666,20 @@ class MultiModelManager: ...@@ -5666,6 +5666,20 @@ class MultiModelManager:
if model_type and model_type in TYPE_MIN_CAP and not caps.to_list(): if model_type and model_type in TYPE_MIN_CAP and not caps.to_list():
setattr(caps, TYPE_MIN_CAP[model_type], True) setattr(caps, TYPE_MIN_CAP[model_type], True)
resolved_type = model_type or (caps.to_list()[0].split("_")[0] if caps.to_list() else "text") resolved_type = model_type or (caps.to_list()[0].split("_")[0] if caps.to_list() else "text")
# A video model carries its geometry: the frame rate it was trained at,
# the frame counts its VAE can decode, how much one render holds and
# whether it continues natively. Without it every client plans video by
# guessing from the model NAME, which is how township ended up with a
# hand-maintained table per family. Never fatal: an unknown family
# degrades to the Wan-shaped default, and a failure here must not cost
# the whole listing.
video_geom = None
if resolved_type == "video" or "video_generation" in caps.to_list():
try:
from codai.models import video_geometry
video_geom = video_geometry.geometry_for(model_id, meta)
except Exception:
video_geom = None
models.append(ModelInfo( models.append(ModelInfo(
id=model_id, id=model_id,
type=resolved_type, type=resolved_type,
...@@ -5679,6 +5693,7 @@ class MultiModelManager: ...@@ -5679,6 +5693,7 @@ class MultiModelManager:
load_mode=meta.get("load_mode"), load_mode=meta.get("load_mode"),
languages=meta.get("languages") or None, languages=meta.get("languages") or None,
supports_translation=meta.get("supports_translation"), supports_translation=meta.get("supports_translation"),
video=video_geom,
)) ))
# --- Models from config (the authoritative source) --- # --- Models from config (the authoritative source) ---
......
"""What a video model can actually emit — frame rate, legal frame counts, continuation.
Three places already knew a piece of this and none of them could tell a client:
``codai/api/video.py:_snap_wan_frames`` (the Wan VAE's 4k+1 grid), ``tools/h3_common``
(MiniMax-H3's 17n+5 clip length) and ``tools/longcat_common`` (LongCat's 93-frame
segment, which a continuation extends by 80). So a caller asking for 250 frames was
silently rounded, and a caller trying to PLAN around the model — how long one scene
can be, how many scenes fill a minute — had no choice but to hardcode a table per
family and keep it in sync by hand. ``tools/gen_township_fighters.py`` did exactly
that, and drifted.
The geometry therefore lives here once, is published per model on ``/v1/models``
(``ModelInfo.video``), and is what the server itself snaps with.
A family's legal TOTAL frame counts are ``frame_base + frame_step * k`` for k >= 0:
wan 1 + 4k the VAE's 4k+1. One render holds ~81 frames; a longer clip is
chained client-side from each part's last frame.
longcat 93 + 80k one 93-frame segment, and each continuation re-renders 13 of
its frames so it only ADDS 80 (``longcat_common.frames_for``).
80 divides by 16, which is why every point on this grid is also
a count block-sparse attention can tile (16n+13): snapping to
the segment grid and keeping BSA on are the same act.
h3 5 + 17k the video VAE's clip length, clamped to 5-15 s.
The numbers are literals rather than imports because ``codai`` runs on a different
Python than the LongCat and H3 workers (3.10 venvs that cannot import ``codai.*``),
and loading their modules by path just to read six constants would drag their
dependencies into every ``/v1/models`` call. ``tests/test_video_geometry.py`` pins
each one against its source of truth so the duplication cannot drift silently.
"""
from typing import Optional
# Families, most specific first: a model is matched by the engine predicate the
# generation path itself uses (so geometry and routing can never disagree), and
# only then by name.
FAMILIES = {
"longcat": {
"family": "longcat",
"label": "LongCat-Video",
# longcat_common: DEFAULT_FPS, DEFAULT_NUM_FRAMES, DEFAULT_COND_FRAMES
"native_fps": 15,
"segment_frames": 93,
"cond_frames": 13,
"frame_base": 93,
"frame_step": 80,
"min_frames": 93,
"max_frames": None, # the server runs the segment loop; no ceiling
"max_frames_per_render": 93,
"default_frames": 93,
# Block-sparse attention tiles the latent in (4, 4, 4) chunks over a 16px
# VAE cell, so a side that is not a multiple of 64 silently falls back to
# dense attention. 16 would decode; 64 is what stays fast.
"side_multiple": 64,
"continuation": "native",
},
"h3": {
"family": "h3",
"label": "MiniMax-H3",
# h3_common: H3_FPS, H3_FRAMES_CHUNK, H3_FRAMES_REMAINDER, H3_MIN/MAX_SECONDS
"native_fps": 24,
"segment_frames": None,
"cond_frames": 0,
"frame_base": 5,
"frame_step": 17,
"min_frames": 124, # ceil(5 s) onto the grid
"max_frames": 345, # floor(15 s) onto the grid
"max_frames_per_render": 345,
"default_frames": 124,
"side_multiple": 32, # H3_CANVAS_MULTIPLE
"continuation": "none",
},
"wan": {
"family": "wan",
"label": "Wan2.x",
"native_fps": 16,
"segment_frames": None,
"cond_frames": 0,
"frame_base": 1,
"frame_step": 4,
"min_frames": 5,
"max_frames": None, # chained client-side, so no total ceiling
# Wan2.2-A14B holds coherence to ~81 frames in ONE call; past that the
# frames visibly jump, which is why longer clips are chained.
"max_frames_per_render": 81,
"default_frames": None, # the request decides; the server has no one default
"side_multiple": 16,
"continuation": "chained",
},
}
# The family used when nothing matches. Wan is the historical shape of every
# diffusers video pipeline the server runs, and its 4k+1 is the common VAE rule.
DEFAULT_FAMILY = "wan"
# models.json keys that override a published field. A new checkpoint whose family
# this table has never heard of is configurable rather than wrong — config is the
# source of truth, here as everywhere else.
CONFIG_OVERRIDES = {
"video_native_fps": ("native_fps", int),
"video_frame_base": ("frame_base", int),
"video_frame_step": ("frame_step", int),
"video_min_frames": ("min_frames", int),
"video_max_frames": ("max_frames", int),
"video_max_frames_per_render": ("max_frames_per_render", int),
"video_side_multiple": ("side_multiple", int),
"video_segment_frames": ("segment_frames", int),
"video_cond_frames": ("cond_frames", int),
}
def family_for(model_name: str = "", config: dict = None) -> str:
"""Which geometry family serves this model.
Asks the workers' own predicates first so a model routed to the LongCat worker
can never be described with Wan's geometry — the backend pin, the alias and the
path all count, exactly as they do when the request is dispatched.
"""
cfg = config or {}
try:
from codai.api import longcat_worker
if longcat_worker.is_longcat_model(model_name, cfg):
return "longcat"
except Exception:
if "longcat" in str(model_name or "").lower():
return "longcat"
try:
from codai.api import h3_worker
if h3_worker.is_h3_model(model_name, cfg):
return "h3"
except Exception:
if "minimax-h3" in str(model_name or "").lower().replace("_", "-"):
return "h3"
return DEFAULT_FAMILY
def geometry_for(model_name: str = "", config: dict = None) -> dict:
"""The published geometry for one video model: family defaults + config overrides."""
cfg = config or {}
raw = cfg.get("_raw_cfg") if isinstance(cfg.get("_raw_cfg"), dict) else None
merged = dict(raw or {})
for k, v in cfg.items():
if k != "_raw_cfg":
merged.setdefault(k, v)
geom = dict(FAMILIES[family_for(model_name, merged)])
for key, (field, cast) in CONFIG_OVERRIDES.items():
if merged.get(key) in (None, ""):
continue
try:
geom[field] = cast(merged[key])
except (TypeError, ValueError):
continue
return geom
def snap_frames(num_frames, geom: dict, mode: str = "nearest") -> int:
"""``num_frames`` moved onto the family's legal grid and into its limits.
``mode`` "nearest" is for a planner choosing a length (a 2-frame move either way
is not worth a whole extra segment); "up" is for honouring a caller who asked for
at least this much. Either way the result is a count the VAE can decode — asking
for one it cannot is how a request comes back a different length than it ordered.
"""
base = int(geom.get("frame_base") or 1)
step = max(1, int(geom.get("frame_step") or 1))
lo = int(geom.get("min_frames") or base)
hi = geom.get("max_frames")
try:
n = int(num_frames)
except (TypeError, ValueError):
n = lo
k = (n - base) / float(step)
k = int(-(-k // 1)) if mode == "up" else int(round(k))
out = base + step * max(0, k)
while out < lo:
out += step
if hi:
while out > int(hi):
out -= step
return max(base, out)
def segments_for(total_frames, geom: dict) -> int:
"""How many model calls a total of ``total_frames`` takes on this family.
LongCat's segment loop runs server-side (one request, native continuation); Wan's
chaining runs in the client. The arithmetic is the same either way: the first call
yields ``max_frames_per_render`` and each later one adds that minus the tail it
re-renders.
"""
per_call = int(geom.get("max_frames_per_render") or 0)
if per_call <= 0:
return 1
added = max(1, per_call - int(geom.get("cond_frames") or 0))
try:
total = int(total_frames)
except (TypeError, ValueError):
return 1
if total <= per_call:
return 1
return 1 + (total - per_call + added - 1) // added
def frames_for(segments, geom: dict) -> int:
"""The inverse of :func:`segments_for` — what ``segments`` calls actually yield."""
per_call = int(geom.get("max_frames_per_render") or 0) or 1
added = max(1, per_call - int(geom.get("cond_frames") or 0))
n = max(1, int(segments or 1))
return per_call + (n - 1) * added
def seconds_to_frames(seconds, geom: dict, fps: Optional[int] = None,
mode: str = "nearest") -> int:
"""A duration in seconds as a legal frame count at ``fps`` (default: the model's own).
Seconds are the honest unit for a plan: a scene is "about twelve seconds", and
pinning frames instead means the duration silently changes the moment the output
rate does.
"""
rate = int(fps or geom.get("native_fps") or 16)
try:
want = float(seconds) * max(1, rate)
except (TypeError, ValueError):
want = 0
return snap_frames(want, geom, mode=mode)
...@@ -155,6 +155,12 @@ class ModelInfo(BaseModel): ...@@ -155,6 +155,12 @@ class ModelInfo(BaseModel):
languages: Optional[List[str]] = None languages: Optional[List[str]] = None
supports_translation: Optional[bool] = None supports_translation: Optional[bool] = None
speaker_diarization: Optional[bool] = None speaker_diarization: Optional[bool] = None
# Video generation: what this model can actually emit — native frame rate, the
# legal frame-count grid, how long ONE render may be and whether continuation is
# native. Published so a client can PLAN around the model (how long a scene can
# be, how many scenes fill a minute) instead of hardcoding a table per family.
# See codai/models/video_geometry.py.
video: Optional[Dict] = None
class ModelList(BaseModel): class ModelList(BaseModel):
......
...@@ -113,6 +113,22 @@ three latent frames that are still noise. The attention is well-defined but it i ...@@ -113,6 +113,22 @@ three latent frames that are still noise. The attention is well-defined but it i
the model was trained on. Measure a known-good clip against the dense path before trusting the model was trained on. Measure a known-good clip against the dense path before trusting
it on anything you care about. it on anything you care about.
### Why 93 + 80k is also the BSA-safe grid
A long request is rendered by the segment loop: the first call emits 93 frames and each
continuation re-renders 13 of the previous segment's frames, so it only *adds* 80. The
totals the server actually emits are therefore **93, 173, 253, 333 …** — and because 80
divides by 16, every one of them is also a `16n+13` count. Staying on the segment grid and
keeping block-sparse attention usable are the same act; a request for 250 frames is
neither, gets rounded to 253, and the plan's own numbers become fiction.
`/v1/models` publishes that grid per model (`video.frame_base`, `video.frame_step`,
`video.max_frames_per_render`, `video.side_multiple`, `video.continuation`) so a client can
snap before it asks. `codai/models/video_geometry.py` is the one table, and the render path
snaps with it too. `tools/gen_township_fighters.py` is the worked example: its clip-length
fields default to `0` = derive from the model, which turns a 70-second match from a dozen
Wan-shaped cuts into four to six LongCat takes without retuning anything by hand.
## Generating ## Generating
```jsonc ```jsonc
......
This diff is collapsed.
...@@ -40,7 +40,10 @@ LC = _contract() ...@@ -40,7 +40,10 @@ LC = _contract()
# ------------------------------------------------------------ the tool # ------------------------------------------------------------ the tool
def test_the_tool_recognises_a_longcat_model(): def test_the_tool_recognises_a_longcat_model():
assert '_longcat = "longcat" in (video_model or "").lower()' in TOOL """By the geometry the server publishes first, and by the name as a fallback: an
entry aliased over an HF repo id is a LongCat model the name cannot identify."""
assert '_geom.get("continuation") == "native"' in TOOL
assert 'or "longcat" in (video_model or "").lower()' in TOOL
def test_the_client_side_split_is_lifted_for_longcat(): def test_the_client_side_split_is_lifted_for_longcat():
...@@ -48,15 +51,17 @@ def test_the_client_side_split_is_lifted_for_longcat(): ...@@ -48,15 +51,17 @@ def test_the_client_side_split_is_lifted_for_longcat():
and better, so the cap must not force parts.""" and better, so the cap must not force parts."""
seg = TOOL[TOOL.index("_longcat = "):TOOL.index("def _render_once")] seg = TOOL[TOOL.index("_longcat = "):TOOL.index("def _render_once")]
assert "_chunk_max = 1 << 30" in seg assert "_chunk_max = 1 << 30" in seg
# …and the non-LongCat path keeps the cap it always had. # …and the non-LongCat path keeps a cap, now the model's own.
assert "SINGLE_CLIP_MAX_FRAMES" in seg and "MODEL_MAX_FRAMES" in seg assert "_single_render_cap(single_clip_max_frames, _geom)" in seg
def test_other_models_are_unaffected(): def test_other_models_are_unaffected():
"""A Wan/VACE run must behave exactly as before.""" """A Wan/VACE run must behave exactly as before: still chained, still at 50 frames
per render, which is what _single_render_cap returns for the wan family."""
seg = TOOL[TOOL.index("_longcat = "):TOOL.index("def _render_once")] seg = TOOL[TOOL.index("_longcat = "):TOOL.index("def _render_once")]
assert "else:" in seg assert "else:" in seg
assert "max(8, min(int(single_clip_max_frames" in seg assert "_single_render_cap(single_clip_max_frames, _geom)" in seg
assert '_RENDER_CHUNK = {"wan": SINGLE_CLIP_MAX_FRAMES}' in TOOL
def test_longcat_continues_from_a_tail_like_vace_does(): def test_longcat_continues_from_a_tail_like_vace_does():
...@@ -72,7 +77,10 @@ def test_the_tail_length_matches_what_longcat_was_trained_with(): ...@@ -72,7 +77,10 @@ def test_the_tail_length_matches_what_longcat_was_trained_with():
expects.""" expects."""
got = int(re.search(r"LONGCAT_COND_FRAMES = (\d+)", TOOL).group(1)) got = int(re.search(r"LONGCAT_COND_FRAMES = (\d+)", TOOL).group(1))
assert got == LC.DEFAULT_COND_FRAMES assert got == LC.DEFAULT_COND_FRAMES
assert "_tail_frames = LONGCAT_COND_FRAMES if _longcat else VACE_TAIL_FRAMES" in TOOL # The model's published cond_frames wins, with the constant as the fallback for a
# front that does not publish geometry yet.
assert 'int(_geom.get("cond_frames") or 0) or LONGCAT_COND_FRAMES' in TOOL
assert "else VACE_TAIL_FRAMES" in TOOL
def test_the_vace_tail_length_is_untouched(): def test_the_vace_tail_length_is_untouched():
......
...@@ -259,24 +259,35 @@ def test_seeding_runs_before_the_management_commands(): ...@@ -259,24 +259,35 @@ def test_seeding_runs_before_the_management_commands():
def test_the_longcat_starter_asks_for_fewer_longer_scenes(): def test_the_longcat_starter_asks_for_fewer_longer_scenes():
"""The scene lengths now come from the MODEL (0 = auto in both starters), so this
asks the same question of the geometry the planner will use."""
wan = TW.BUILTIN_TEMPLATES["Wan - many short scenes"] wan = TW.BUILTIN_TEMPLATES["Wan - many short scenes"]
lc = TW.BUILTIN_TEMPLATES["LongCat - few long scenes"] lc = TW.BUILTIN_TEMPLATES["LongCat - few long scenes"]
# Longer scenes…
assert lc["clip_min_frames"] > wan["clip_max_frames"]
# …at LongCat's own frame rate, not the Wan-era compromise. # …at LongCat's own frame rate, not the Wan-era compromise.
assert lc["fps"] == 15 and wan["fps"] == 8 assert lc["fps"] == 15 and wan["fps"] == 8
w_lo, w_hi = TW._clip_frame_range(wan["clip_min_frames"], wan["clip_max_frames"],
TW.video_geometry("wan2.2"), "clip", wan["fps"])
l_lo, l_hi = TW._clip_frame_range(lc["clip_min_frames"], lc["clip_max_frames"],
TW.video_geometry("longcat"), "clip", lc["fps"])
# Longer scenes…
assert l_lo > w_hi
# …so the same 45s short needs far fewer of them. # …so the same 45s short needs far fewer of them.
scenes_wan = 45 / (wan["clip_max_frames"] / wan["fps"]) assert 45 / (l_hi / lc["fps"]) < 45 / (w_hi / wan["fps"]) / 2
scenes_lc = 45 / (lc["clip_max_frames"] / lc["fps"])
assert scenes_lc < scenes_wan / 2
def test_the_longcat_frame_counts_obey_the_vae_rule(): def test_the_longcat_frame_counts_obey_the_vae_rule():
"""(frames - 1) divisible by 4 — the same rule the server checks before a long run, """(frames - 1) divisible by 4 — the same rule the server checks before a long run,
and the one LongCat's own 93-frame segment satisfies.""" and the one LongCat's own 93-frame segment satisfies. The starter no longer carries
the numbers, so the rule is asked of what auto derives."""
lc = TW.BUILTIN_TEMPLATES["LongCat - few long scenes"] lc = TW.BUILTIN_TEMPLATES["LongCat - few long scenes"]
for key in ("clip_min_frames", "clip_max_frames", "single_clip_max_frames"): geom = TW.video_geometry("longcat")
assert (lc[key] - 1) % 4 == 0, key for kind in ("clip", "intro", "outcome"):
for n in TW._clip_frame_range(0, 0, geom, kind, lc["fps"]):
assert (n - 1) % 4 == 0, (kind, n)
# And on the segment grid, which is stricter and is what keeps
# block-sparse attention usable.
assert (n - 93) % 80 == 0, (kind, n)
assert (TW._single_render_cap(lc["single_clip_max_frames"], geom) - 1) % 4 == 0
def test_the_longcat_starter_selects_the_model(): def test_the_longcat_starter_selects_the_model():
...@@ -309,10 +320,13 @@ def test_the_starters_cover_the_outcome_clips_too(): ...@@ -309,10 +320,13 @@ def test_the_starters_cover_the_outcome_clips_too():
def test_the_longcat_outcomes_obey_the_vae_rule_as_well(): def test_the_longcat_outcomes_obey_the_vae_rule_as_well():
lc = TW.BUILTIN_TEMPLATES["LongCat - few long scenes"] lc = TW.BUILTIN_TEMPLATES["LongCat - few long scenes"]
for key in ("outcome_min_frames", "outcome_max_frames"): lo, hi = TW._clip_frame_range(lc["outcome_min_frames"], lc["outcome_max_frames"],
assert (lc[key] - 1) % 4 == 0, key TW.video_geometry("longcat"), "outcome", lc["fps"])
# 93 is one whole LongCat segment: the shortest honest ask. for n in (lo, hi):
assert lc["outcome_min_frames"] == 93 assert (n - 1) % 4 == 0, n
# An outcome is split across two shots, so its total is at least two segments —
# 93 is one whole LongCat segment and the shortest honest ask for either half.
assert lo >= 93 * 2 - 13
def test_the_starters_leave_the_image_side_stages_alone(): def test_the_starters_leave_the_image_side_stages_alone():
......
"""What a video model can emit, published per model on /v1/models.
Three modules knew a piece of this and none of them could tell a client: the Wan VAE's
4k+1 grid lived in codai/api/video.py, MiniMax-H3's 17n+5 in tools/h3_common.py, and
LongCat's 93-frame segment in tools/longcat_common.py. A client planning video therefore
guessed from the model NAME and kept its own table — which is how the township tool ended
up tuned for a model it was no longer using.
codai/models/video_geometry.py is that knowledge once. Because the LongCat and H3 workers
run in their own 3.10 venvs and cannot be imported from here, the numbers are literals,
so the first tests below pin each one against the module that owns it: duplication is
tolerable, silent drift is not.
"""
import importlib.util
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from codai.models import video_geometry as G
def _by_path(name, rel):
spec = importlib.util.spec_from_file_location(name, ROOT / rel)
m = importlib.util.module_from_spec(spec)
sys.modules[name] = m
spec.loader.exec_module(m)
return m
LC = _by_path("lc_common_geom", "tools/longcat_common.py")
H3 = _by_path("h3_common_geom", "tools/h3_common.py")
# --------------------------------------------------------------- pinned to source
def test_longcat_numbers_come_from_longcat_common():
g = G.FAMILIES["longcat"]
assert g["native_fps"] == LC.DEFAULT_FPS
assert g["segment_frames"] == LC.DEFAULT_NUM_FRAMES
assert g["cond_frames"] == LC.DEFAULT_COND_FRAMES
assert g["frame_base"] == LC.DEFAULT_NUM_FRAMES
# A continuation re-renders the tail, so a segment only ADDS num - cond.
assert g["frame_step"] == LC.DEFAULT_NUM_FRAMES - LC.DEFAULT_COND_FRAMES
def test_the_longcat_grid_is_what_the_segment_loop_actually_yields():
g = G.FAMILIES["longcat"]
for segments in (1, 2, 3, 4, 7):
assert G.frames_for(segments, g) == LC.frames_for(segments)
assert G.snap_frames(LC.frames_for(segments), g) == LC.frames_for(segments)
def test_every_longcat_grid_point_is_a_count_bsa_can_tile():
"""80 divides by 16, so 93 + 80k is always 16n+13 — the counts bsa_problems accepts.
This is the whole reason snapping to the segment grid is worth doing: a count off
the grid drops the pass onto dense attention, which is slower and OOMed a 24GB card.
"""
g = G.FAMILIES["longcat"]
for k in range(8):
frames = g["frame_base"] + g["frame_step"] * k
assert not LC.bsa_problems(frames, 512, 832), frames
def test_h3_numbers_come_from_h3_common():
g = G.FAMILIES["h3"]
assert g["native_fps"] == H3.H3_FPS
assert g["frame_step"] == H3.H3_FRAMES_CHUNK
assert g["frame_base"] == H3.H3_FRAMES_REMAINDER
assert g["side_multiple"] == H3.H3_CANVAS_MULTIPLE
assert g["min_frames"] == H3.snap_frames(H3.H3_MIN_SECONDS * H3.H3_FPS)
assert g["max_frames"] == H3.snap_frames(H3.H3_MAX_SECONDS * H3.H3_FPS)
@pytest.mark.parametrize("n", [1, 120, 150, 200, 345, 360, 400])
def test_h3_snapping_agrees_with_the_worker(n):
"""Up, not nearest: the worker snaps a request UP to what its VAE can decode."""
assert G.snap_frames(n, G.FAMILIES["h3"], mode="up") == H3.snap_frames(n)
@pytest.mark.parametrize("n", [1, 5, 6, 48, 50, 81, 250])
def test_wan_snapping_agrees_with_the_render_path(n):
from codai.api.video import _snap_wan_frames
assert G.snap_frames(n, G.FAMILIES["wan"]) == _snap_wan_frames(n)
def test_wan_holds_eighty_one_frames_in_one_call():
"""Past ~81 the frames visibly jump, which is why longer clips are chained."""
assert G.FAMILIES["wan"]["max_frames_per_render"] == 81
assert G.FAMILIES["wan"]["continuation"] == "chained"
# ------------------------------------------------------------------ family match
@pytest.mark.parametrize("name,cfg,family", [
("longcat", {}, "longcat"),
("LongCat-Video-Avatar", {}, "longcat"),
("Wan-AI/Wan2.2-I2V-A14B-Diffusers", {}, "wan"),
("MiniMax-H3", {}, "h3"),
("minimax_h3_fast", {}, "h3"),
("something-else", {}, "wan"),
])
def test_the_family_is_recognised_by_name(name, cfg, family):
assert G.family_for(name, cfg) == family
def test_a_backend_pin_beats_the_name():
"""An entry aliased over an HF repo id is routed by its backend, so it must be
described by its backend too — geometry and routing cannot be allowed to disagree."""
assert G.family_for("my-video-model", {"backend": "longcat"}) == "longcat"
assert G.family_for("my-video-model", {"backend": "h3"}) == "h3"
def test_an_alias_is_enough():
assert G.family_for("org/checkpoint-42", {"alias": "longcat"}) == "longcat"
def test_the_raw_config_is_unwrapped():
"""build_runtime_kwargs drops every engine-specific key except inside _raw_cfg,
which is exactly where a backend pin ends up on the video path."""
geom = G.geometry_for("org/x", {"_raw_cfg": {"backend": "longcat"}})
assert geom["family"] == "longcat"
def test_an_unknown_family_degrades_to_the_common_vae_rule():
geom = G.geometry_for("brand-new-video-model", {})
assert geom["family"] == G.DEFAULT_FAMILY
assert (geom["frame_base"], geom["frame_step"]) == (1, 4)
# --------------------------------------------------------------- config overrides
def test_config_overrides_win():
"""Config is the source of truth: a checkpoint this table has never heard of is
configurable rather than permanently mis-described."""
geom = G.geometry_for("mystery", {
"video_native_fps": 30, "video_frame_base": 9, "video_frame_step": 8,
"video_max_frames_per_render": 200, "video_side_multiple": 64})
assert geom["native_fps"] == 30
assert G.snap_frames(100, geom) == 97 # 9 + 8k
assert geom["max_frames_per_render"] == 200
assert geom["side_multiple"] == 64
def test_a_junk_override_is_ignored_not_fatal():
geom = G.geometry_for("longcat", {"video_native_fps": "soon"})
assert geom["native_fps"] == G.FAMILIES["longcat"]["native_fps"]
def test_geometry_for_does_not_mutate_the_table():
G.geometry_for("longcat", {"video_native_fps": 7})
assert G.FAMILIES["longcat"]["native_fps"] == LC.DEFAULT_FPS
# ------------------------------------------------------------------- the maths
def test_snapping_never_lands_below_the_minimum():
for fam in G.FAMILIES.values():
assert G.snap_frames(0, fam) >= fam["min_frames"]
assert G.snap_frames(-5, fam) >= fam["min_frames"]
def test_snapping_respects_a_ceiling():
assert G.snap_frames(10_000, G.FAMILIES["h3"]) <= G.FAMILIES["h3"]["max_frames"]
# LongCat has none: the server runs the segment loop as long as asked.
assert G.FAMILIES["longcat"]["max_frames"] is None
assert G.snap_frames(10_000, G.FAMILIES["longcat"]) > 1000
def test_nonsense_frames_do_not_raise():
assert G.snap_frames(None, G.FAMILIES["wan"]) >= 5
assert G.snap_frames("many", G.FAMILIES["wan"]) >= 5
def test_segments_and_frames_are_inverses():
for fam in ("longcat", "wan"):
g = G.FAMILIES[fam]
for k in range(1, 6):
assert G.segments_for(G.frames_for(k, g), g) == k
def test_seconds_become_a_legal_count_at_the_asked_rate():
lc = G.FAMILIES["longcat"]
assert G.seconds_to_frames(11.5, lc) == 173 # two segments at 15fps
assert G.seconds_to_frames(22.0, lc) == 333 # four
wan = G.FAMILIES["wan"]
assert G.seconds_to_frames(6.25, wan, fps=8) == 49
# The POINT of seconds: the same scene length at a different rate is more frames.
assert G.seconds_to_frames(6.25, wan, fps=16) == 101
def test_seconds_default_to_the_models_own_rate():
lc = G.FAMILIES["longcat"]
assert G.seconds_to_frames(11.5, lc) == G.seconds_to_frames(11.5, lc, fps=15)
# ---------------------------------------------------------------- what's published
def test_the_listing_can_carry_it():
from codai.pydantic.textrequest import ModelInfo
geom = G.geometry_for("longcat", {})
info = ModelInfo(id="longcat", type="video",
capabilities=["video_generation"], video=geom)
assert info.model_dump()["video"]["frame_step"] == 80
def test_a_non_video_model_carries_nothing():
from codai.pydantic.textrequest import ModelInfo
assert ModelInfo(id="some-llm", type="text").video is None
def test_the_manager_publishes_geometry_for_video_models():
"""Read from the source: building a real manager needs a GPU and a config tree."""
src = (ROOT / "codai/models/manager.py").read_text()
assert "from codai.models import video_geometry" in src
assert "video=video_geom," in src
assert 'if resolved_type == "video" or "video_generation" in caps.to_list():' in src
def test_the_render_path_snaps_with_the_published_grid():
"""Otherwise what the server advertises and what it rounds to could drift."""
src = (ROOT / "codai/api/video.py").read_text()
body = src.split("def _snap_wan_frames", 1)[1].split("\ndef ", 1)[0]
assert "video_geometry.snap_frames" in body
assert "return max(5, k * 4 + 1)" in body, "the inline fallback must stay"
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment