Skip to content

fix(media-use): repoint the dead videogen tier, demote past unusable local models - #3509

Merged
jrusso1020 merged 2 commits into
mainfrom
fix/media-use-videogen-dead-tier
Aug 26, 2026
Merged

fix(media-use): repoint the dead videogen tier, demote past unusable local models#3509
jrusso1020 merged 2 commits into
mainfrom
fix/media-use-videogen-dead-tier

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What

Three things, in the local-model registry behind the media-use skill:

  1. LOCAL_MODELS.videogen's large tier pointed at dgrauet/ltx-2.3-mlx-bf16, which returns HTTP 401 and cannot be downloaded. Repointed to dgrauet/ltx-2.3-mlx-q8.
  2. Added the recovery that was missing: a model the table selects but that cannot actually run now demotes to the next fitting tier instead of failing the capability.
  3. Both videogen sizeMB values were describing a download that never happens, and nothing told the user what a pull would cost before it started. Corrected to measured repo totals, and the size is now disclosed at the moment someone decides to install, with a free-space check.

Why

The dead entry was not dormant. rankedByPreference sorts by descending needs.ramMB when no rank is set, so the largest fitting tier is tried first, by design. On any machine clearing 32 GB available RAM (meetsSpecs gates on availableRamMB ?? ramMB) selection landed on the 401'ing entry, ltxVideoGenerate caught the failure and returned a bare null, and because ltx.local is last in ["heygen.video", "ltx.local"] and network providers are skipped under --local-only (registry.mjs:206), local video generation failed outright — with the working q4 tier sitting right below it, never tried.

Worth recording why it survived review: the table landed with "live verification on a 24GB M-series Mac", and a 24 GB machine cannot select a 32 GB tier. The entry was structurally unreachable on the only machine that validated it. The unit fixtures inherit the same ceiling — fittingSpecs is 20000 MB — so every existing test in ltx-video-provider.test.mjs exercised the medium tier alone. When a table's entries are gated on the verifier's own hardware, everything above their ceiling is untested no matter how careful the pass was.

How

The repoint, and the size numbers

sizeMB described a targeted hf download --include subset. No run ever gets that subset. Both videogen invokes pass a bare repo id to --model, and upstream resolve_model_dir() (ltx_pipelines_mlx/utils/_orchestration.py:35-40) is:

def resolve_model_dir(model_dir: str | Path) -> Path:
    path = Path(model_dir)
    if path.exists():
        return path
    return Path(snapshot_download(str(model_dir)))

No allow_patterns, no ignore_patterns — the whole repo lands regardless of what was pre-fetched. Corrected to the measured totals (?blobs=true, every blob summed):

entry was now measured
ltx-2.3-mlx-q8 45000, then 28800 87500 87,511,991,375 B = 87.51 GB
ltx-2.3-mlx-q4 20000 59700 59,686,429,583 B = 59.69 GB

A correction to my own earlier reasoning in this PR. I justified 28800 as matching "the sibling q4 entry's convention". That was wrong in the way that matters: the convention itself described a subset the runner does not honor, so q4's 20000 was wrong too. Both are fixed together rather than leaving one convention on each side.

Two independent confirmations that the download is the complete repo:

  • The file's own comment already said "pointing tools at the repo blind snapshot-downloads the lot (60 GB q4, 88 GB q8)" — three lines above the fields that contradicted it.
  • The original report measured it: "the q4 cache ended at 56 GB and q8 at 82 GB, against repo totals of 59.7 GB and 87.5 GB." Those reconcile exactly once read as GiB (du labels GiB as G): 59.69 GB = 55.6 GiB, 87.51 GB = 81.5 GiB. So it is not "fetches what it needs" — it fetches everything, both times.

The --include recipe is removed rather than repaired, because it is both ineffective and insufficient. Insufficient because --two-stage is "dev model + CFG at half-res, upscale, distilled LoRA refine" (upstream's own cli.py:393 help text), so it needs transformer-dev and transformer-distilled and spatial_upscaler_x2; the documented distilled-only subset has none of the first and neither of the last two. --distilled needs an upscaler too. The q4 tier that was live-verified on a 24 GB Mac only worked because the download is unfiltered.

I deliberately did not take the other available route — making invoke pre-resolve a filtered artifact set to a local path. Getting that include-list right requires validating it on a 32 GB+ Mac I do not have, which is the exact failure this PR exists to fix. It also saves less than it appears: two-stage needs ~59 of the 87.5 GB, and the slack is duplicate variants (transformer-distilled vs -1.1, two 7.61 GB LoRA copies).

Telling the user what a download costs

describeDownload(sizeMB) in specs.mjs names the size and the directory the weights land in, and checks free space with statfs against that directoryweightsCacheDir() follows huggingface_hub's own precedence (HF_HUB_CACHEHUGGINGFACE_HUB_CACHEHF_HOME/hub~/.cache/huggingface/hub), because a check against cwd measures the wrong filesystem. The cache dir usually does not exist until the first download and statfs throws on a missing path, so it walks up to the deepest existing ancestor.

Three deliberate choices:

  • A tier that will not fit is still offered, with a plain statement that it will not fit. Hiding it would make a machine that could free up 90 GB look like it has no large tier.
  • Unknown free space reports as unknown, never as zero — otherwise an unreadable filesystem scares someone off a download that would have worked.
  • Best-effort throughout. Cleanup and probing never manufacture a new failure.

Wired into both providers' install hints (the actual accept moment a user reads), the runLocalModel install payload (now carrying sizeMB), and describeModelLadder.

On this box the large tier correctly reads:

downloads ~87.5GB of weights to /home/ubuntu/.cache/huggingface/hub,
but only 13.8GB is free there, so it will NOT fit as-is

The demotion, and cleaning up after it

selectModelLadder(capability, specs, { preferTier }) returns every fitting model best-first; selectModel's pick is now defined as that list's head, so the existing single-select behaviour is unchanged by construction. Three call sites previously selected exactly one model and failed terminally — all three walk the ladder now:

site capability latent failure it had
ltx-video-provider.mjs videogen the reported bug
mflux-provider.mjs imagegen same shape; its 32 GB / 64 GB tiers are equally unverifiable on a 24 GB machine
local-run.mjs tts / asr / upscale fish-speech missing cost you Kokoro; a broken seedvr2 cost you real-esrgan

Every demotion is logged, not silent — a quietly smaller model would otherwise be indistinguishable from the tier the machine nominally qualified for.

Because each attempt mints its own timestamped temp path, a partial artifact from a failed tier was orphaned rather than overwritten, and a lower tier then succeeding hid it. Both providers now discard the partial before demoting, guarded so a file that cannot be removed never masks the generate failure it came from. Video is the material case: a partial mp4 is large.

local-run.mjs is deliberately excluded from that cleanup. Its out is caller-provided and identical across attempts, so a partial is overwritten rather than orphaned, and unlinking a path the caller named would be a footgun. The rule the two providers follow is: clean up what you allocate.

Also fixed: the install string both videogen entries share ended at uv sync --all-extras, which leaves the entry point in .venv/bin. The "ltx-2-mlx not on PATH" hint was naming a command that following its own instruction would not put on PATH.

Test plan

  • Unit tests added/updated
  • Manual testing performed — see limits below
  • Documentation updated (if applicable) — the table's own notes carry the verification status

553/553 across skills/**/*.test.mjs (72 files, matching CI's own find-based enumeration).

Added, beyond the ladder/demotion coverage:

  • specs.test.mjs — cache-dir precedence, the statfs walk-up to the deepest existing ancestor, unknown-vs-zero, and the will-not-fit wording.
  • ltx-video-provider.test.mjs / mflux-provider.test.mjs — the failed tier's partial is removed while the returned artifact survives; one discard per attempt on the all-fail path; an unremovable partial still surfaces the real failure; each install hint states that tier's download cost.
  • local-run.test.mjs — the install recommendation carries sizeMB and states the download size.
  • local-models.test.mjs — a guard that an invoke naming an owner/repo model agrees with the entry id, so a half-done repoint cannot select one model and download another.

Mutation-tested rather than trusted green. Every new guard was removed in turn and the suite confirmed red:

mutation result
discardPartial removed from the LTX catch 2 fail
discardPartial removed from the mflux catch 2 fail
will-not-fit branch removed from describeDownload 1 fail
freeSpaceMB ancestor walk-up removed 1 fail
sizeMB stripped from the install payload 2 fail
size disclosure removed from the LTX hint 1 fail
repoint reverted in the invoke only (id q8, invoke bf16) consistency guard fails
demotion continue replaced with return null both demotion tests fail
meetsSpecs dropped from the ladder filter 6 fail, incl. pre-existing selectModel tests

Gates run locally: oxfmt --check clean (733 files), oxlint 0 warnings / 0 errors, git diff --check clean, check-tracked-artifacts passed, check-skill-mirror OK (24 files byte-for-byte), lint-skills 32 files clean. skills-manifest.json is the pre-commit hook's regeneration of the media-use hash.

A correction to this PR's earlier test plan: it claimed lint-registry-items was "372 items clean". That was wrong — it reports 26 pre-existing errors on main (font-face declarations in compositions/). They are outside this diff, which touches zero composition or registry files, and the script is referenced by no workflow and is not in the lint chain, so it is not a CI gate. Flagging it rather than restating a claim that does not hold.

Not verified — flagged deliberately: the q8 tier has had no live run; no 32 GB+ Apple Silicon machine was available, and its notes say so in the table. Shipping it unverified is safe precisely because of change 2 — a wrong tier now costs one failed attempt instead of the whole local path. A reviewer with a 32 GB+ Mac can confirm the --two-stage --low-ram combination.

A product question I did not decide: an 87.5 GB large tier against q4's 59.7 GB is a real tradeoff, not a rounding difference. Whether that tier is worth offering at all is a product call, not a bug fix, so this PR makes the cost honest and visible and leaves the decision open.

Known follow-up, deliberately not in this PR: resolve.mjs:395 builds ctx from six fields, so ctx.width / ctx.height / ctx.frames / ctx.preferTier are all unreachable — every local video is 512x320 x 33 frames, and the manual tier override is dead. That is a separate change to the resolve surface and gets its own PR.

  • Rames Jusso

…models

`LOCAL_MODELS.videogen`'s `large` tier named `dgrauet/ltx-2.3-mlx-bf16`, which
returns HTTP 401 and cannot be downloaded at all. It was not a dormant entry:
`rankedByPreference` sorts by descending `needs.ramMB` when no `rank` is set,
so the largest fitting tier is tried FIRST by design. Any machine clearing
32 GB *available* RAM selected the dead entry, `ltxVideoGenerate` caught the
failure and returned a bare `null`, and since `ltx.local` is last in
`["heygen.video", "ltx.local"]` and network providers are skipped under
`--local-only` (`registry.mjs:206`), local video generation failed outright
instead of falling back to the tier that works.

It survived review because the table landed with "live verification on a 24GB
M-series Mac" - and a 24 GB machine cannot select a 32 GB tier, so that entry
was unreachable on the only machine that validated it. The unit fixtures
inherit the same ceiling (`fittingSpecs` is 20000MB), so every existing test
exercised the medium tier alone.

Two changes:

1. Repoint to `dgrauet/ltx-2.3-mlx-q8` (reachable) and correct `sizeMB` from
   45000 to 28800. Measured against the HF API: the q8 repo totals 87.5 GB,
   and the registry's own targeted `--include` subset is 28.76 GB. That
   matches the sibling q4 entry's convention (`sizeMB: 20000` vs a measured
   19.48 GB subset), so 45000 was wrong under either reading. `--low-ram` is
   added because the entry's own note calls it required at this tier's 32 GB
   floor, and the invoke omitted it.

2. A repoint alone is one bad URL from a repeat, so add the missing recovery.
   `selectModelLadder` returns every fitting model best-first;
   `selectModel`'s pick is now defined as that list's head. All three sites
   that previously selected exactly one model and failed terminally walk the
   ladder instead, demoting past a tier that cannot run here - gated weights,
   runner off PATH, an OOM at a tier that nominally fits:

   - `ltx-video-provider.mjs` (videogen, the reported failure)
   - `mflux-provider.mjs` (imagegen - same shape, and its 32 GB/64 GB tiers
     are equally unverifiable on a 24 GB machine)
   - `local-run.mjs` (tts/asr/upscale - `fish-speech` missing should still
     get you Kokoro)

   Every demotion is logged rather than silent, so a quietly smaller model is
   never mistaken for the tier the machine nominally qualified for.

Also fixes the `install` string both videogen entries share: it ended at
`uv sync --all-extras`, which leaves the entry point in `.venv/bin`, so the
"`ltx-2-mlx` not on PATH" hint named a command that following the instruction
would not put on PATH.

The q8 tier is NOT live-verified - no 32 GB+ Apple Silicon machine was
available - and its notes say so. Shipping it unverified is safe precisely
because of change 2: a wrong tier now costs one failed attempt, not the whole
local path.

- Rames Jusso

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head review at 90df164a22089aa0c6cefdc7fc00ffdee54853a6.

The ladder construction is clean: selectModel() is now defined from the same best-first list the retrying providers consume, and the 40 GB fixtures finally exercise the previously unreachable large rung (local-models.mjs:269-284, ltx-video-provider.test.mjs:102-226). The preferTier pin also preserves an explicit no-demotion escape hatch.

Blockers

  1. skills/media-use/scripts/lib/local-models.mjs:137-147sizeMB: 28800 and the note do not describe the path this entry actually runs. The invoke passes the repo ID directly to ltx-2-mlx; upstream resolve_model_dir() calls unfiltered snapshot_download(repo). The current q8 repository is 87,511,991,375 bytes, so a qualifying machine downloads roughly 87.5 GB, not a 28.8 GB targeted subset. More importantly, --two-stage --low-ram needs both transformer-dev and transformer-distilled plus the x2 spatial upscaler; the 28.8 GB distilled-only subset described above the table is not sufficient for this invocation. Please either actually pre-resolve the exact two-stage artifact set to a local path and record that measured size, or report the full snapshot size/runtime behavior.

  2. skills/media-use/scripts/lib/ltx-video-provider.mjs:40-66 and skills/media-use/scripts/lib/mflux-provider.mjs:59-100 — the new retry boundary allocates a temp output per attempt, but every failure continue abandons that path. Generators can create a partial MP4/PNG before exiting nonzero (OOM/timeout is one of the advertised demotion cases); the lower tier can then succeed while the failed artifact remains hidden in the temp directory. Remove failed-attempt outputs before continuing and pin the partial-file case. The LTX path is especially material because a partial video can be large.

Local evidence: 35 focused tests passed; the full skills suite reproduced 538/538; git diff --check passed. I also verified the q8 repo and the runner flags against their current primary upstream sources.

Verdict: REQUEST CHANGES
Reasoning: The fallback mechanics and tests are strong, but the shipped large-tier download contract is currently off by tens of gigabytes and the new retry loops leak partial outputs on their intended failure paths.

— Magi

…discard failed partials

Addresses review feedback on #3509 (CHANGES_REQUESTED at 90df164), plus the
follow-on ask to tell the user what a download costs before they accept it.

1. `sizeMB` described a targeted `--include` subset that no run ever gets.
   Both videogen invokes pass a repo id to `--model`, and upstream
   `resolve_model_dir()` (`ltx_pipelines_mlx/utils/_orchestration.py:35-40`)
   calls `snapshot_download(repo)` with no `allow_patterns`, so the full repo
   lands regardless of what was pre-fetched. Corrected to measured repo
   totals: q8 87500 (87,511,991,375 B) and q4 59700 (59,686,429,583 B). q4 was
   wrong the same way at 20000, so both are fixed together rather than leaving
   one convention on each side.

   My earlier claim that 28800 "matches the sibling q4 entry's convention" was
   wrong in the way that matters: the convention itself described a subset the
   runner does not honor. The file's own comment already said "blind
   snapshot-downloads the lot (60 GB q4, 88 GB q8)" three lines above the
   fields that contradicted it, and the original report measured it too ("the
   q4 cache ended at 56 GB and q8 at 82 GB"), which reconciles exactly once
   read as GiB: 59.69 GB = 55.6 GiB, 87.51 GB = 81.5 GiB. So the download is
   the complete repo both times, not a partial fetch.

   Removed the `--include` recipe rather than repairing it: it is ineffective
   (the runner refetches at generate time) and insufficient (`--two-stage` is
   "dev model + CFG at half-res, upscale, distilled LoRA refine" per upstream's
   own help text, so it needs transformer-dev AND transformer-distilled AND
   spatial_upscaler_x2; `--distilled` needs an upscaler too). The q4 tier
   verified on a 24 GB Mac only worked BECAUSE the download is unfiltered.

2. Nothing told the user what they were agreeing to before a tool started
   pulling tens of GB. `describeDownload()` in `specs.mjs` names the size and
   the directory the weights land in, and checks free space with `statfs`
   against that directory rather than cwd, since the weights do not land in
   cwd. A tier that will not fit is still offered, with a plain statement that
   it will not fit: hiding it would make a machine that could free up space
   look like it has no large tier. Unknown free space reports as unknown, not
   as zero. Wired into both providers' install hints, the `runLocalModel`
   install payload (now carrying `sizeMB`), and `describeModelLadder`.

3. Each retry attempt mints its own timestamped temp path, so a partial
   artifact from a failed tier was orphaned rather than overwritten, and a
   lower tier then succeeding hid it. Both providers discard the partial before
   demoting, guarded so a file that cannot be removed never masks the generate
   failure it came from. Video is the material case: a partial mp4 is large.

   `local-run.mjs` is deliberately unchanged here. Its `out` is caller-provided
   and identical across attempts, so a partial is overwritten rather than
   orphaned, and unlinking a path the caller named would be a footgun. The rule
   the two providers follow is: clean up what you allocate.

Tests: 553/553 across `skills/**/*.test.mjs` (+15). New coverage pins the
cleanup (failed tier's partial removed, returned artifact survives, one discard
per attempt on the all-fail path, an unremovable partial still surfaces the
real failure) and the disclosure (cache-dir precedence, statfs walk-up to the
deepest existing ancestor, unknown-vs-zero, and the will-not-fit wording).
Every new guard mutation-tested: removing any one of them turns tests red.

- Rames Jusso

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed blocker-fix head 6195d5a7f2caf65aa8eadbe864d66ddf4b436a22.

Both original blockers are closed:

  • skills/media-use/scripts/lib/local-models.mjs:117-150 now records the actual unfiltered snapshot contract: q4 59.7 GB and q8 87.5 GB, with the ineffective/insufficient targeted-subset recipe removed. The two-stage note correctly names dev + distilled + x2 upscaler, and the ladder/install payload now exposes sizeMB.
  • ltx-video-provider.mjs:22-33,67 and mflux-provider.mjs:41-52,101 remove failed-attempt partials before demotion. The cleanup is best-effort, never touches a successful returned artifact, and is pinned for fail-then-succeed, all-fail, success, and unlink-failure paths.

The added download disclosure is also coherent: Hugging Face cache precedence is modeled explicitly, statfs walks to the deepest existing ancestor, and insufficient/unknown free space is reported without silently removing a tier.

Local verification reproduced 56 focused tests and the complete skills suite at 553/553; git diff --check passed. GitHub reports no failing or pending checks at this head.

No remaining blocker.

Verdict: APPROVE
Reasoning: The model-size contract now matches the actual runtime download, retry cleanup owns every failed temp artifact, and the new disclosure/free-space behavior is bounded and tested.

— Magi

@jrusso1020
jrusso1020 merged commit 8392e84 into main Aug 26, 2026
75 checks passed
@jrusso1020
jrusso1020 deleted the fix/media-use-videogen-dead-tier branch August 26, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants