Skip to content
  1. Oct 28, 2026
    • Stefy Lanza (nextime / spora )'s avatar
      size a model by the card that was asked for, and see every kind of load · dd9ba587
      Stefy Lanza (nextime / spora ) authored
      
      
      Two bugs of the same shape: a signal that already existed and was not consulted.
      
      Same-path siblings. Two cards can share one weights path and differ only in
      settings -- Wan2.2-I2V-A14B-Lightning and the plain Wan2.2-I2V-A14B both point
      at Wan-AI/Wan2.2-I2V-A14B-Diffusers, declaring 22 GB and 151 GB. The manager
      has disambiguated exactly this since the lisa/lisa-32k case, through
      `requested_id`, and NEITHER placement caller passed it. So the Lightning card
      was sized by its 151 GB sibling, read as ~127 GB over free VRAM -- far outside
      the borderline band -- and went straight to CPU offload without trying the
      card, where it computed at 0% GPU for 26 minutes holding 37 GB of host RAM.
      Measured: 97.4 GB without the id, 14.1 GB with it, which is the difference
      between exile and residency on a 24 GiB card. Quantizing the experts was
      necessary but not sufficient; the ESTIMATE was still the wrong card's.
      
      The leak watcher's load signal. _load_in_progress() consulted only the
      manager's ready event, which diffusers image/video loads never touch -- they
      load in codai/api/{images,video}.py. A 12-minute multi-shard load therefore
      looked idle, its monotonically rising RSS looked exactly like a leak, and the
      mitigation ladder ran against a load doing nothing wrong ("RSS 19.8/43.0 GB
      LEAK SUSPECTED", every poll). It now also consults the loading reservations,
      which cover those paths by construction.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      dd9ba587
    • Stefy Lanza (nextime / spora )'s avatar
      send a step callback only to a pipeline that takes one · b3aaec96
      Stefy Lanza (nextime / spora ) authored
      
      
      diffusers added `callback_on_step_end` per pipeline rather than across the
      board, and audio_gen passed it unconditionally. StableAudioPipeline does not
      take it, so every request raised
      
          StableAudioPipeline.__call__() got an unexpected keyword argument
          'callback_on_step_end'
      
      AFTER the model had finished loading -- one unsupported keyword cost the whole
      load and read like a broken model rather than a wrong argument.
      
      Asked of the signature, not kept as a list of pipeline names: such a list is
      wrong again at the next diffusers release. The pre-0.25 `callback` spelling is
      adapted so those pipelines keep their progress, and a pipeline that supports
      neither says so, because silently losing progress reporting and mid-run
      cancellation should not be invisible.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      b3aaec96
    • Stefy Lanza (nextime / spora )'s avatar
      prefer VRAM by default, and let the suite run where production runs · 1ad16f15
      Stefy Lanza (nextime / spora ) authored
      
      
      Offload: the default becomes auto-borderline, not auto. `auto` condemns a model
      on its ESTIMATE -- if used_vram_gb exceeds free VRAM it goes straight to CPU
      offload without ever trying the card -- and that estimate is a measurement from
      an earlier load in some cards and a hand-entered round number in others
      (LTX-2.3-PinkCherry declared exactly 24.0 on a 24 GiB card). So a conservative
      figure permanently exiled a model that would have fit.
      
      The cost of being wrong is asymmetric, which is what decides it: guessing "fits"
      wrongly costs one failed attempt and a fallback, while guessing "does not fit"
      wrongly costs a CPU copy of every component and a PCIe trip per request for the
      life of the install -- 28-37 GB of host RAM per video render on a 54 GB box with
      no cgroup limit to stop it. The dropdown is relabelled by intent and reordered,
      because the old labels were the trap: `auto` read as the sensible choice while
      being the pessimistic one.
      
      Tests: three files hardcoded /storage/coderai, so collection FAILED outright
      inside the production image and every test in them had only ever been exercised
      against the host's dependency set -- which is diffusers 0.37.0.dev0 with no
      torchsde and no PyAV, against the image's 0.42.1 with both. The four Krea 2 edit
      tests were silently skipping for want of Krea2Pipeline and now run (15 passed,
      0 skipped in-image). Paths are derived from __file__ instead.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      1ad16f15
    • Stefy Lanza (nextime / spora )'s avatar
      let the model loader authenticate to the Hub, like the diarization worker always has · 46b4988e
      Stefy Lanza (nextime / spora ) authored
      
      
      The diarization worker resolved an HF token from the environment or a protected
      file in the config dir, and gated pyannote repos worked. The MODEL LOADING path
      had no such resolution: the host runner forwards HF_TOKEN from the host
      environment, which is only set if whoever launched the container exported it,
      and on this install it was not -- so huggingface_hub went out unauthenticated
      for every from_pretrained.
      
      That is invisible until a repo is gated, and then it fails as 401 "must be
      authenticated", which reads exactly like terms that have not been accepted even
      when they have. On stabilityai/stable-audio-open-1.0 the account had access and
      the token on disk could fetch the weights with curl, while the engine loading
      them could not -- so the obvious reading was a licence problem, and it was a
      credentials-plumbing problem.
      
      The resolution moves into codai/hf_cache.py, which already owns how a process
      talks to the Hub, and is applied from the same entry point as the cache
      settings: both have to be in place before huggingface_hub is imported, and an
      install that configured one has configured the other in the same directory. The
      file/env list is kept identical to the worker's, and the worker now calls it,
      because two answers to "where is the token" is how one path authenticates and
      another does not. Its per-request override still wins. Never logged: it ends up
      in request headers, and a test holds that.
      
      Also torchsde, which Stable Audio Open's CosineDPMSolverMultistepScheduler is
      built on. diffusers imports it lazily, so without it the model downloads its
      3.8 GB in full and only then fails at request time, after the wait.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      46b4988e
    • Stefy Lanza (nextime / spora )'s avatar
      C3: keep the soundtrack LTX generates, and stop reporting every file as silent · 45724c7a
      Stefy Lanza (nextime / spora ) authored
      
      
      LTX-2.x is the one video family here with an audio VAE and a vocoder, so its
      output is LTX2PipelineOutput(frames=..., audio=...) -- audio generated jointly
      with the frames and therefore actually in step with them. Only `frames` was
      read, so every LTX render came out silent and the only way to get sound was to
      generate some separately with another model and mux that on top, which does not
      match the picture. The generated track is now muxed in BEFORE the other
      post-processing steps, because every one of them re-encodes the frames and the
      track belongs to these exact ones.
      
      `add_audio` becomes tri-state so "say nothing" and "say no" differ: unset keeps
      what the model generated, false gives a silent video, true runs the older
      separate-generation path. It defaulted to False, which cannot be told from an
      explicit false -- so a straight default would have dropped the soundtrack. None
      is falsy everywhere False was, so existing callers are unaffected.
      
      Found while testing it: _has_audio tried `import av` under a bare
      `except: return False`, and PyAV is in no main requirements file -- only the
      isolated LongCat venv's. On an install without it EVERY file answered "no
      audio", and because it gates _mux_original_audio, a clip's own sound was
      silently dropped from every upscale and interpolate. The audio was there,
      nothing could see it, and nothing said so. ffprobe cannot be missing the way
      PyAV can, since ffmpeg is what does the muxing, so it is the fallback, and a
      failure of both is logged instead of reported as silence.
      
      Also: one of my own tests asserted a minimum number of ffmpeg progress reports.
      `-progress` emits on a time interval, so a small encode can finish having
      emitted a single block -- it passed alone and failed under the full suite. It
      now asserts what is guaranteed: frames counted, counted upward.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      45724c7a
    • Stefy Lanza (nextime / spora )'s avatar
      C4: give Krea 2 the reference-image edit diffusers does not ship · 96a84b69
      Stefy Lanza (nextime / spora ) authored
      
      
      Krea2Pipeline.__call__ takes a prompt and no image, and the family's only other
      entry points in diffusers 0.42 are the Krea2Turbo modular blocks. So the card
      with the identity-edit LoRA had its weights ready and no way to be asked
      "change THIS picture" -- and because _task_of() finds no img2img mapping for
      the family, an edit request fell through to text-to-image and silently ignored
      the image it was given.
      
      The edit is done IN CONTEXT, not by denoising from the reference: the reference
      is VAE-encoded once, kept clean, and appended to hidden_states beside the
      canvas, so identity survives the whole trajectory instead of being worn away as
      the canvas resolves. That is what the family's edit LoRAs are trained against,
      and it is why `strength` has no meaning here -- there is no partial noising to
      control, which also makes _is_instruction_edit classify it correctly straight
      off its signature.
      
      Telling the token groups apart is the whole trick. position_ids is (seq, 3):
      the canvas keeps (0, h, w) and the reference repeats its grid at plane 1, so
      the rotary embedding separates "this pixel of the canvas" from "this pixel of
      the reference" while keeping them aligned. Identical coordinates would make the
      two indistinguishable. The builder is module-level and pure so it can be
      checked without the 12B weights, and on a build whose diffusers has no Krea 2
      at all -- importing the module never breaks such a build.
      
      Built with from_pipe over the loaded components: a Krea 2 transformer is 12B
      and a second load would mean an eviction on every alternation between
      generating and editing. The reference is encoded with mode(), not sample(), so
      the same edit of the same picture does not differ run to run.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      96a84b69
    • Stefy Lanza (nextime / spora )'s avatar
      13, 13b and A4: progress a client can actually use, and a still that is not 70s · e3bb5c46
      Stefy Lanza (nextime / spora ) authored
      
      
      13. A request may name itself (`request_id` in the body, or an X-Request-Id
      header) and poll /v1/{images,video,audio}/progress?request_id=... for its OWN
      steps. One global slot per media type meant two callers could not tell their
      progress apart, so a client stopped reading them altogether and showed no step
      progress at all while the server was drawing. The name is used as the TASK id,
      so this is a VIEW over the existing task registry -- which already tracks
      step/total/status per task under a lock and prunes finished ones -- rather than
      a second place progress is written. Claimed once, because a request registers
      a short "loading" task as well and both adopting the id would serve the client
      whichever happened to be last. Unknown id = 404, so a client stops polling on
      a definite answer instead of reading somebody else's progress.
      
      13b. The encode reports frames written (`encoding 1200/2400 frames`) across the
      band it owns, so the longest step of a render stops looking like a stall; and
      the job's own log, kept all along but unreachable, is served as a tail in the
      status document plus GET /v1/video/compose/{id}/log?since=N for the whole
      thing incrementally. The record holds the live list, not a snapshot.
      
      A4. 72s per 768x1344 still -> 21.4s, measured on an idle 3090. The card carried
      `offload_strategy: "model"`, which is the model page's DEFAULT, so
      enable_model_cpu_offload() ran on a model that fits a 23.56 GB card whole and
      paid PCIe for it on every request. `auto` loads on GPU and falls back to
      offload only on OOM. The doc's other hypothesis is ruled out rather than
      assumed: the live progress endpoint reports total=10, so the card's step count
      IS applied, and an explicit steps=10 takes the same 21.2s.
      
      Also: `models.civitai_cache_dir`, configured like hf_cache_dir and
      gguf_cache_dir because it is the same kind of thing. Unset, these
      multi-gigabyte checkpoints land beside the other caches -- on this host a
      140 GB volume, which a 13.5 GB checkpoint took to 99% until the next model
      could not write even its config fetch ("No space left on device"). That is
      what was actually stopping Z-Image-Turbo-IntoRealism from loading at all.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      e3bb5c46
    • Stefy Lanza (nextime / spora )'s avatar
      green the whole suite: 17 failures, none of them a product bug · acf9ab5c
      Stefy Lanza (nextime / spora ) authored
      Every one encoded something that had since changed, or depended on a package
      this checkout does not have. Each was read against the code before being
      touched, and two turned out to be real defects:
      
      - ggml_rpc.available() returned the bare exception, so a model naming RPC
        servers refused with "No module named 'llama_cpp'" -- true, but naming
        neither the capability nor the fix. Every refusal now says both.
      - building a BitsAndBytesConfig without bitsandbytes installed raised
        PackageNotFoundError from importlib.metadata. The IMPORT is guarded; only
        construction consults metadata, so a Vulkan-only or CPU install got a
        traceback naming neither the model nor the setting. Now reported as loudly
        as an unrecognised mode, because the consequence is the same: full precision.
        The 2-bit path already did this for optimum-quanto.
      
      The rest were stale expectations:
      - the broker protocol inverted (the client speaks first: one read, no separate
        ack), gained keepalive pings and an immediate `pending` frame, and renamed
        its stream envelopes to chunk/done. Added coverage for the status gate that
        suppresses `pending`, which nothing tested.
      - /admin/api/status was removed in def78c18
      
       and the front owns status now, so
        its on-the-way-out coercion had nothing left to coerce. Retargeted to the
        invariant that survives: entries are well formed when MADE.
      - main()'s default boot became the front/engine split, so the app instance
        reaches uvicorn in the ENGINE path now.
      
      And four were brittle rather than wrong, each failing for something it was not
      testing: a 4000-character source window that stopped covering the raise when a
      comment was added above it (now the function's own source); a hand-written
      Config stand-in that broke on every new section the product read (now falls
      back to the real defaults, per section); a generic `selectedId` used as a proxy
      for "the pipeline UI is gone" that unrelated code then adopted; and a pool test
      that needed pypdfium2 to exercise what a BUSY pool does.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      acf9ab5c
    • Stefy Lanza (nextime / spora )'s avatar
      14c + 14d: wrap text instead of shrinking it, and let a clip's sound come from its own file · 3ebc2bd5
      Stefy Lanza (nextime / spora ) authored
      
      
      14c. A text overlay was one drawtext line sized from the frame height, so a
      long text ran off a vertical frame, and a pre-wrapped text was drawn as a
      single line because an explicit newline was never a break. `overlays[].fit`
      takes none (the default, today's behaviour) | wrap | shrink, and a newline now
      breaks whatever the fit. Wrapping emits one drawtext per line, placed as a
      single left-aligned block measured from the widest line, rather than letting
      each line centre itself ragged. `captions.fit` takes shrink (the default) |
      wrap, folding a too-wide row's trailing words onto a new row instead of
      shrinking that event -- on a vertical video shrinking can go below readable.
      Word order is preserved, so the karaoke highlight still walks the words in the
      order they are spoken.
      
      Both fitters now MEASURE the result instead of scaling the size by the width
      ratio. Advance is very nearly linear in size, which is why the shortcut looks
      right, but hinting rounds each glyph's advance to whole pixels and the error
      compounds over a long string: a 95-character line computed to fit at size 8
      measured 440 px against a 300 px budget. A test caught it. _fit_size carried
      the same assumption and shrink is its default, so it is fixed the same way.
      
      14d. `visuals[].audio_src` names a separate audio-only file -- DASH sites serve
      picture and sound as separate streams, so the best video of a clip often has no
      audio track and a caller that cannot mux had to skip it. The picture path is
      deliberately left alone: stage 3 reuses it as the visual's video, and an
      audio-only file there would render a scene with no image. Lengths in
      audio_mix="clip" come from whatever carries the sound.
      
      Also: the dialogue tests no longer depend on a TTS model being installed. They
      test how a dialogue scene is VALIDATED and say so -- their portraits are inline
      "so the test does not depend on what is saved on this install" -- but three of
      them failed standalone with 501 "narration needs a TTS model" without reaching
      the validation they exercise. Only the inventory is faked, not the check.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      3ebc2bd5
    • Stefy Lanza (nextime / spora )'s avatar
      read the music response as the type the endpoint returns · 6729e974
      Stefy Lanza (nextime / spora ) authored
      
      
      audio_generate answers an AudioGenerationResponse, a pydantic model, and
      _generate_music read it with .get(). Every compose asking for AI music raised
      "'AudioGenerationResponse' object has no attribute 'get'", warned "rendered
      without music", and then SUCCEEDED -- silent. That is why it lasted: the render
      never failed, so only someone watching the finished video would notice, and
      "AI composed" music has been off in production the whole time.
      
      Both shapes are accepted, since the same call is reachable through paths that
      answer a plain dict, and a non-dict item is coerced rather than assumed.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      6729e974
    • Stefy Lanza (nextime / spora )'s avatar
      make an ffmpeg we started unable to outlive us · 42d68ca1
      Stefy Lanza (nextime / spora ) authored
      
      
      run() kills its child on cancel and on timeout, but both need this process to
      be alive to act. SIGKILLed -- an OOM, a container stop, an interrupted test run
      -- no finally block runs, the child is reparented to init, and nothing owns it
      any more. That is not a dormant process: a filter graph that never reaches EOF
      spins at 100% CPU indefinitely. Two orphans from interrupted test runs were
      found on a dev host having burned 15 h and 11 h of CPU, with no parent to
      attribute them to.
      
      PR_SET_PDEATHSIG has the kernel kill the child as the parent dies, so the
      outcome stops depending on our cleanup running at all. Best effort, a no-op
      where prctl is unavailable, and tested by SIGKILLing a real parent rather than
      by asserting the flag is set.
      
      Also bound the audio mix at 20x its own length with a two-minute floor. Mixing
      a few seconds of PCM takes milliseconds, so the two-hour video backstop meant a
      wedged mix sat for two hours; it now gives up in two minutes.
      
      Plus a test that every name identifying a whisper.cpp runner reaches it -- the
      runner's id, a shared alias, and the weights path /v1/models advertises, by
      full path, basename and stem -- and that naming the weights spreads across a
      shared alias instead of serialising a pool configured for capacity.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      42d68ca1
    • Stefy Lanza (nextime / spora )'s avatar
      choose the dtype default from a device that has been detected · 9646dd69
      Stefy Lanza (nextime / spora ) authored
      
      
      NvidiaBackend.__init__ leaves self.device None. The loader picked the precision
      default as `'fp16' if self.device == 'cuda' else 'fp32'` at line 897 and only
      detected the device at line 1029, so the FIRST load in a fresh engine process
      defaulted to fp32 on a CUDA card -- while a second load in the same process
      inherited 'cuda' from the first and quietly got fp16. The same model loaded at
      two precisions depending on load order.
      
      fp32 costs twice the VRAM of the dtype asked for, which on a 24 GB card is the
      whole headroom argument for quantizing in the first place: Qwen3.5-9B is
      carded load_in_4bit with an estimate of 7.795 GB and measured 14.403 GB, a
      1.85x gap that is the fp32 tell. FlashAttention also refuses fp32 outright, so
      transformers warned twice at load, cast the inputs back to fp32 at inference,
      and the model answered every request with "FlashAttention only support fp16 and
      bf16 data type" -- and before that error was surfaced at all, with an empty
      completion. It had been unusable since 2026-09-30.
      
      Also resolve flash_attn by PRESENCE rather than truthiness: `config.get(k) or
      global` cannot express "off", so a card saying `flash_attention: false` fell
      through to the global default. Not the cause here (the global is false too),
      but the same class of bug one layer up.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      9646dd69
    • Stefy Lanza (nextime / spora )'s avatar
      let the eviction ladder see an image load while it runs · a3446118
      Stefy Lanza (nextime / spora ) authored
      
      
      The video path reserves a model's VRAM estimate for its whole load window so a
      concurrent load sees the card as spoken-for. The image path registered only a
      Tasks-page entry, so for as long as a load took, the card looked free.
      
      Measured 2026-10-28: Krea-2-RealDream held 18.8 GiB of a 23.56 GiB card for the
      194 s of its load, ensure_vram_for granted a CrisperWhisper reservation against
      VRAM that was already gone, and every captioned scene of a concurrent compose
      answered "CUDA out of memory. Tried to allocate 86.00 MiB". Both ends had been
      fixed in isolation -- the STT path reserves, the worker releases -- and neither
      helped, because the model they were negotiating over never declared itself.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      a3446118
    • Stefy Lanza (nextime / spora )'s avatar
      an out-of-band engine is a model: card it, track it, and queue behind it · 124941a4
      Stefy Lanza (nextime / spora ) authored
      
      
      The lip-sync engines were outside the ledger entirely. A render is gigabytes on
      the card for minutes, in a process the manager did not start, so the next model
      to load read the card as emptier than it is, took what it found, and one of the
      two died. They are models.json rows now -- video_models, backend "lipsync",
      capabilities lip_sync -- configurable on the models page like anything else,
      and a run goes through acquire_stt_backend, the same door LongCat and the STT
      workers use. The footprint is sampled during the run (these are third-party
      scripts that report nothing about themselves) and kept as a high-water mark, so
      the next reservation is measured. The registration is dropped when the process
      exits: one-shot work that stayed in the ledger would claim VRAM nobody holds.
      
      The bigger half is a defect that was never about lip sync. acquire_stt_backend
      POPS the instance pool, and _is_key_busy looks only at that pool -- so every
      model registered through it reads as permanently idle. LongCat mid-generation,
      a CrisperWhisper transcription, a lip-sync render: eviction was not choosing to
      take the card from busy work, it could not see the work was there. LongCat's own
      source notes this and works around it inside its releaser; nothing else did.
      
      So multi_model_manager grows external_busy(key), and _is_key_busy consults it.
      The existing eviction logic then does the right thing with no further change:
      the first pass skips a busy model, the second waits for the active one. An
      in-flight render is something to queue behind, not a candidate -- cleanup()
      waits for the process and never terminates it, because a half-finished render
      is worth nothing and the request that triggered the eviction is not more
      important than the one already running.
      
      Also: the Civitai redirect fix moved the seam two tests patched, so they stopped
      intercepting and started reaching the real site -- which is why they failed on
      HTTP 404 rather than on the 401/403 pages they assert about. Every request now
      leaves through one function, which is the thing to patch.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      124941a4
    • Stefy Lanza (nextime / spora )'s avatar
      give the lip-sync engines an operator, and let a component be one file · f035e581
      Stefy Lanza (nextime / spora ) authored
      
      
      Two things that were only reachable from a python -c.
      
      The lip-sync engines had no management surface at all: no status, no install,
      no removal. They are not models.json entries on purpose -- each runs in its own
      interpreter as a subprocess, so a row in a model section would put them in the
      VRAM ledger as something the manager can load, which it cannot -- but that is a
      reason to give them their own surface, not none. So: a tab on the models page,
      three admin endpoints behind require_admin like every other model action, and
      an install that runs in the background while the page polls. The log is kept
      because when an install fails, WHERE it failed is the whole answer: a wheel
      that will not build reads nothing like a 401. "Rebuild venv" keeps the weights,
      since a dependency change does not alter six gigabytes.
      
      `component_models` could only name a repo. A community fine-tune is very often
      ONE file -- a retrained transformer with no repo around it, the base supplying
      the VAE, the text encoder and the scheduler -- and from_pretrained cannot open
      one, so such a component could only be named if somebody had published a
      converted directory tree for it. It now accepts a single-file or civitai:
      reference.
      
      And the Civitai resolver picked the LARGEST file of a version. Real Dream's
      Krea 2 V3 publishes the same weights three times: fp8 13.46 GB, int8 13.18 GB,
      nvfp4 8.12 GB. Largest-wins lands on fp8 by a 0.28 GB margin nobody chose, and
      could as easily have landed on nvfp4, which needs Blackwell and cannot load on
      an Ampere card at all. A reference may now name the file, and without one the
      choice is made among the builds this GPU can run, by precision rather than size.
      
      Two smaller ones found by using it. urllib replays headers across a redirect,
      so Civitai's signed R2 URL received a bearer token as well and answered 400 --
      which looks exactly like a bad token, and meant adding a token BROKE downloads
      that had worked without one. curl strips it; now so does this. And the cache
      has its own root (CODERAI_CIVITAI_CACHE) instead of living under the HF cache,
      which is how a second GGUF directory grew inside one.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      f035e581
    • Stefy Lanza (nextime / spora )'s avatar
      make the MuseTalk install reach a working venv · e740d6e8
      Stefy Lanza (nextime / spora ) authored
      
      
      Four pins and one probe, each found by building it rather than reading about it.
      
      mmpose's RTMO head imports mmdet unconditionally although mmpose lists it as
      optional, so `import mmpose.apis` dies without it; mmdet then asserts
      mmcv < 2.2.0 at import; openmmlab publishes a cp311 mmcv 2.1.0 wheel on exactly
      one index, cu121/torch2.1. So the torch pin is 2.1.2 and it is dictated by mmcv,
      not by MuseTalk. mmengine imports pkg_resources to find mmpose's config
      directory, which setuptools dropped in 81, making setuptools<81 a runtime pin.
      
      chumpy, which mmpose depends on, has a setup.py that does `import pip` and
      cannot build in an isolated environment. Named per package rather than turning
      isolation off wholesale, which would let everything else build against a
      half-installed venv.
      
      The venv probe was wrong, not the install: it ran a bare interpreter, so
      face_detection -- vendored under musetalk/utils, not a package -- came back
      missing and a complete install was reported broken. It runs against the install
      root with the engine's own PYTHONPATH now, as a real run does, and asks for
      mmpose.apis rather than mmpose, since the package imports fine and the submodule
      is what fails.
      
      Also: uv's HTTP timeout is too short for hundred-megabyte wheels off third-party
      indexes, and the abort reads as a corrupt wheel rather than a timeout. And a
      per-run directory is swept after a day -- a failed run keeps its task file and
      frames, which is what you read to find out why, but not for ever.
      
      Verified live: MuseTalk v15 lip-synced a 2s 480x480 clip in 92s, 48 frames with
      the audio muxed, and the successful run removed its own directory.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      e740d6e8
    • Stefy Lanza (nextime / spora )'s avatar
      refuse a quantized model the card cannot hold, instead of proving it slowly · d94b144e
      Stefy Lanza (nextime / spora ) authored
      
      
      _reject_offloaded_quantized already catches a 4-bit model whose weights landed
      on the CPU -- that is what stopped Qwen3.5-9B answering every request with "".
      But it runs at the BOTTOM of the device_map ladder. So a full-GPU attempt that
      OOMs still walks every rung: 427 weight shards across GPU and host RAM, many
      minutes of it, before raising an error that was knowable when the ladder
      started. The line printed immediately above the fallback says as much --
      "quantized kernels cannot read offloaded weights" -- and then falls through to
      the ladder anyway.
      
      The other half is that the attempt which failed was never retried honestly. It
      ran against whatever happened to be free on a shared card, which is a lottery;
      another model's residency is a reason to evict, not a reason to mis-place this
      one. Reproduced on the live server today: 7.2 GB free because an embedding model
      and a whisper-server held the rest, and nothing asked them to move.
      
      So a quantized model that fails full-GPU now evicts and tries once more, and
      refuses immediately if that still does not fit, naming the three things that
      help. Unquantized models keep the ladder: offload is legitimate for dense
      weights, only slower.
      
      This makes a failure faster and louder rather than slower and quieter, which is
      a behaviour change on a live path.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      d94b144e
    • Stefy Lanza (nextime / spora )'s avatar
      install the engines the presenter has been preferring all along · 998d6d97
      Stefy Lanza (nextime / spora ) authored
      
      
      LatentSync and MuseTalk have been selectable by name for a while, and
      `engine: "auto"` has ranked both ahead of wav2lip whenever there was a clip to
      drive -- wav2lip works at 96x96 and upscales, which is the soft mouth on a 1080p
      reel. Nothing ever installed them, so every selection ended at "neither on PATH
      nor installed", and the call that would have been made could not have worked
      either: LatentSync requires its checkpoint and UNet config as well as the three
      paths, and MuseTalk takes a YAML task list and writes into a result directory
      rather than taking paths at all.
      
      Coderai orchestrates what it serves, so it installs these. The inference source
      is vendored under third_party/ and pinned to a commit, for the reason LongCat's
      is: an image someone else pulls has to run without network access to a third
      party. The weights come from the Hub on first use, never during a render. Each
      engine gets its own venv on its own python, because both pin stacks the server
      cannot host.
      
      Upstream MuseTalk takes its BiSeNet face-parsing checkpoint off a Google Drive id
      and a pytorch.org URL; both come from a Hub mirror here, so the whole set has one
      provenance.
      
      _managed_engine_ready used to answer "an entrypoint and any .pt anywhere", which
      reads an interrupted download as a working install. It now asks the module that
      knows the file list, and the old heuristic keeps covering an engine an operator
      installed by hand. /v1/video/presenter/engines reports what an uninstalled engine
      is short of, so a client can tell "not yet" from "never here".
      
      Verified live: LatentSync installed from nothing and lip-synced a 2s 480x480 clip
      to real narration in 136s.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      998d6d97
    • Stefy Lanza (nextime / spora )'s avatar
      reserve what a transcription costs, and let the card be taken back · f738df2c
      Stefy Lanza (nextime / spora ) authored
      
      
      Three CrisperWhisper models were configured and they stacked: each is a resident
      Whisper-large in its own process, so the card held two of them plus the server's
      text and embedding models until one OOMed inside the encoder's softmax. Nothing
      here was the model's fault.
      
      The reservation taken before a load was a hard-coded 3 GB -- roughly the fp16
      weights, and about a third of what a verbatim word-timestamp pass peaks at, since
      that holds the encoder's attention for a whole 30s window. The worker now reports
      its own peak and the figure is recorded as a high-water mark, so the next
      reservation is measured rather than guessed.
      
      The eviction ladder had no way to reach a worker's VRAM: it lives in another
      process, and acquire_stt_backend pops the model pool, so the manager's busy
      signal cannot see a transcription either. There is an external releaser now --
      idle workers go at once, a busy one is waited out to the drain budget and only
      then taken -- and asking for one model stops its idle siblings.
      
      A cold start was invisible while it lasted. Free VRAM comes from the driver, so a
      worker downloading its shards looks like room; live traffic loaded into it and the
      transcription OOMed on a card nobody over-committed on purpose. The load window
      holds a reservation now.
      
      whisper-server already evicts and knows when it is idle, but it sized itself at
      file size x 1.1, which counts the weights and not whisper.cpp's compute buffers.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      f738df2c
  2. Oct 27, 2026
    • Stefy Lanza (nextime / spora )'s avatar
      a component_quantization value nobody understands must not vanish · 08df8843
      Stefy Lanza (nextime / spora ) authored
      
      
      The map takes 4bit / 8bit / 2bit or a .gguf path. Anything else was skipped
      without a word -- and one unusable entry empties the mapping, which takes the
      global load_in_4bit with it, so the component loads from the base repo at full
      precision while the operator believes it comes from the file they named.
      
      LTX-2.5 named a bf16 Gemma-4 12B for its text_encoder. The quantization config
      came back None, the 12B loaded unquantized, and every generation ended with
      the card full: 23.53 GiB of 23.56 GiB in use, unable to allocate 30 MiB, on a
      model whose own measured footprint is 14.45 GB. Four strategies and about an
      hour of 20-minute attempts, all of them really the same missing line.
      
      Now it says so, and names component_models -- the key that does what such a
      value was meant to do.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      08df8843
    • Stefy Lanza (nextime / spora )'s avatar
      a GGUF component is pre-built too, so placement must treat it as one · bcdd5ffb
      Stefy Lanza (nextime / spora ) authored
      
      
      video.py already knew the hazard and wrote it down: accelerate's device_map
      only dispatches components it LOADS, so one handed to from_pretrained keeps
      whatever device it is on with no offload hook, and the forward pass hits
      "index on cuda:0, weights on cpu". It acted on that only for pipeline-cache
      injections, while a GGUF component arrives exactly the same way.
      
      LTX-2.5 therefore failed under every strategy it tried -- auto, balanced,
      model and pure-disk, three restarts and half an hour each -- always with
      
          Expected all tensors to be on the same device, but got index is on
          cuda:0, different from other tensors on cpu
      
      and video_resident_experts, the flag that exists for precisely this, was
      never consulted because the gate in front of it was false.
      
      An explicit offload_strategy is still respected; only the strategies that let
      accelerate place components itself are overridden.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      bcdd5ffb
    • Stefy Lanza (nextime / spora )'s avatar
      the component probe asked a cache that had no reason to hold the answer · a2633773
      Stefy Lanza (nextime / spora ) authored
      
      
      The LTX-2.5 key reconciliation shipped, the retry failed identically, and the
      repair had never run. It needs the component's config.json to know which names
      the class expects, and _component_dir looked only in the local snapshot --
      which, for a component whose weights come from a GGUF, is precisely the
      subfolder nobody downloads. 20 GB of weights the file replaces are not worth
      fetching; a few hundred bytes of config are.
      
      Cache first, then one hf_hub_download of that component's config.json. Same
      lesson as a cache-only existence check on a rented pod: an answer of "not
      here" from a cache is not an answer about the repo.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      a2633773
    • Stefy Lanza (nextime / spora )'s avatar
      reconcile the attention norms LTX-2.5 spells the other way round · b14e4ccd
      Stefy Lanza (nextime / spora ) authored
      
      
      The repair that fixed LTX-2.3's prompt-adaln names could only strip a suffix.
      LTX-2.5's GGUF needs a whole segment renamed: it calls the attention norms
      `q_norm` / `k_norm` where LTX2VideoTransformer3DModel expects `norm_q` /
      `norm_k`. 724 parameters, one to one, the same tensors -- and unreconciled
      they stay on the meta device, so a 37-minute load ends with
      
          Cannot copy out of meta tensor; no data!
      
      about weights the file was carrying all along. Measured by diffing the GGUF's
      4349 tensors against the class's 4091 parameters: nothing was absent, 724 were
      simply spelled differently.
      
      Both directions, because nothing says which spelling the next exporter picks,
      and both repairs now run in one pass -- a checkpoint can need each for a
      different family of keys. The existing guard is unchanged: a key moves only
      when the class does not want its current name, does want the renamed one, and
      nothing already holds the target.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      b14e4ccd
    • Stefy Lanza (nextime / spora )'s avatar
      say "no such file" when a GGUF component is not on disk · 07235001
      Stefy Lanza (nextime / spora ) authored
      
      
      diffusers reports a missing single-file checkpoint as
      
          Invalid `pretrained_model_name_or_path` provided. Please set it to a
          valid URL.
      
      which sends you to check the repo id. The actual cause, measured on LTX-2.5,
      was an entry pointing into ~/.cache/huggingface on the HOST -- a path the
      container does not mount -- so the file existed everywhere except in the
      process trying to open it. Every other part of that model was ready.
      
      The path is probed before the load and named in the failure, with the reason
      it is usually absent.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      07235001
    • Stefy Lanza (nextime / spora )'s avatar
      a model card could cross the line the request path refuses to · 920b3636
      Stefy Lanza (nextime / spora ) authored
      
      
      _apply_sampler turns a cross-family sampler into a 400 that explains itself:
      flow-matching and DDPM schedulers parameterise the trajectory differently, so
      one cannot stand in for the other. The acceleration config applied its
      `scheduler` with no such check, and a card is exactly where that mistake gets
      made and never noticed.
      
      Krea-2-Turbo's entry asked for EulerDiscreteScheduler on a flow-matching
      model. The swap succeeded silently and the generation then died with
      
          EulerDiscreteScheduler.set_timesteps() got an unexpected keyword
          argument 'mu'
      
      which names neither the card nor the model, arrives from inside diffusers, and
      reads like a library bug. Now it is refused and logged, naming both
      schedulers, and the model keeps its own -- acceleration is an optimisation and
      has always been allowed to decline.
      
      One set of family names, imported by both paths, so they cannot drift.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      920b3636
    • Stefy Lanza (nextime / spora )'s avatar
      a two-tuple from encode_prompt is not always (embeds, negative) · fb53791e
      Stefy Lanza (nextime / spora ) authored
      
      
      Krea 2 returns (prompt_embeds, prompt_embeds_mask), and the generic path read
      any 2-tuple as SD 1.x's (embeds, negative_embeds) -- so it handed the mask
      over as a negative prompt and the pipeline answered
      
          If `prompt_embeds` are provided, `prompt_embeds_mask` also have to be
          passed.
      
      which reads as a missing argument rather than a mislabelled one, and arrives
      wrapped in "Failed to load image model" two and a half minutes into the load.
      
      Told apart by the pipeline's own __call__ signature rather than a list of
      family names, and the answer for those families is to pre-encode nothing:
      weighted 75-token windows are a CLIP-pair feature that means nothing to them,
      which is why Z-Image already takes that path.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      fb53791e
    • Stefy Lanza (nextime / spora )'s avatar
      keep this python's native libraries off a foreign interpreter's loader path · de4745f4
      Stefy Lanza (nextime / spora ) authored
      
      
      With PYTHONHOME stripped, SadTalker's 3.10 got as far as importing torch and
      died there:
      
          ImportError: /opt/coderai/local-libs/libtorch_python.so:
          undefined symbol: PyObject_GetOptionalAttrString
      
      a CPython 3.13 API. The container launcher exports LD_LIBRARY_PATH as
      /opt/coderai/python/lib:/opt/coderai/local-libs:<the server venv's nvidia
      wheels>, so a child on another python loads THIS one's extension modules --
      the same bug as PYTHONHOME, one layer down, and it hides the same way: by hand
      in the same container the import works, because an interactive shell has none
      of it.
      
      Filtered rather than dropped. An isolated venv ships its own torch and CUDA
      wheels, but a system CUDA or anything the operator put on that path is theirs
      to keep -- and an empty LD_LIBRARY_PATH is not an unset one, it means the
      current directory.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      de4745f4
    • Stefy Lanza (nextime / spora )'s avatar
      a lip-sync engine dies on the environment the job hands it · 728511f6
      Stefy Lanza (nextime / spora ) authored
      
      
      Measured on the live server, on the first presenter request that ever ran:
      
          sadtalker failed (rc=1): init_fs_encoding: failed to get the Python codec
          of the filesystem encoding / No module named 'encodings'
      
      The shims on PATH run the bundled python 3.10 venv, and the process that
      spawns them exports PYTHONHOME=/opt/coderai/python -- the standalone 3.13. The
      child dies before executing a byte, the job falls back to the scene's visuals,
      and the warning blames the engine. Same root cause as the ACE-Step worker's
      "requirements missing", so it uses the same helper; the deliberate PYTHONPATH
      these repos need is set after cleaning, pointing at the checkout rather than
      inheriting the server's.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      728511f6
    • Stefy Lanza (nextime / spora )'s avatar
      cut the presenter out of the room they were filmed in · f5c55cd4
      Stefy Lanza (nextime / spora ) authored
      
      
      presenter.remove_background was accepted and then warned about: the one field
      of the compose contract with nothing behind it. A pip bubble that is a circle
      of someone's kitchen reads as a sticker; the same bubble with the person cut
      out reads as a presenter.
      
      The mask is the whole feature -- it replaces the bubble's shape mask, so
      composite_pip needed one line and composite_split needed the B-roll behind the
      person's half. Two engines: a TorchScript matting model (an RVM export is the
      obvious one, and being recurrent it is steadier over time) when
      CODERAI_MATTING_MODEL names one, else torchvision's DeepLabV3, which is
      already in the stack and needs no configuration. It borrows the GPU under the
      same lease lip-sync takes and is dropped as soon as the scene's mask is
      written -- a 100 MB segmenter is not something the manager should have to
      evict.
      
      Nothing about it is fatal: a scene that cannot be cut out still gets its
      presenter, filmed room and all, and says so in the warnings.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      f5c55cd4
    • Stefy Lanza (nextime / spora )'s avatar
      bake the venv onto the image instead of building the image twice · c93075fc
      Stefy Lanza (nextime / spora ) authored
      
      
      The ACE-Step variant was only reachable by running the whole build again with
      --with-acestep: ~20 GB of bundle recopied to add 6.4 GB that has nothing to do
      with it, and two builds that can disagree. The overlay makes the relationship
      literal -- the baked image IS the slim one plus this venv, one layer on top,
      461s instead of a second 15-minute build.
      
      The build-time check runs the venv's python with PYTHONHOME unset. The image
      sets it to the standalone 3.13, and a 3.12 inheriting it dies before executing
      a byte, so without `env -u` the check would fail on the trap rather than on
      the venv it is meant to prove.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      c93075fc
    • Stefy Lanza (nextime / spora )'s avatar
      a worker's own interpreter dies on the environment the engine hands it · 4b223f7b
      Stefy Lanza (nextime / spora ) authored
      
      
      Live on zeiss, /v1/audio/generate answered in 0.25s with "ACE-Step
      requirements missing", which was not the problem: the venv was there and
      imported fine by hand in the same container.
      
      The container's per-engine launchers export PYTHONHOME=/opt/coderai/python
      (the standalone 3.13); PID 1 and an interactive docker exec do not. A 3.12
      interpreter inheriting it aborts before running a byte -- "Current thread ...
      <no Python frame>" -- so the worker's import probe failed instantly, and the
      caller went down the rebuild path to report a missing file instead. Reproduced
      both ways before changing anything.
      
      longcat_worker had already solved this for its 3.10 venv, behind a docstring
      saying coderai does not set those variables. It does. The helper is shared now
      (codai/api/venv_env), longcat delegates to it, and every ACE-Step spawn --
      probe, both venv-creation routes, the install, the service -- goes through it.
      The probe also prints what the child said rather than returning a bare False.
      
      Then response_format: declared as url | b64_wav | b64_mp3, and ignored. The
      response was chosen by whether a file directory was configured, so a client
      asking for base64 silently got a URL -- the same bug /v1/images had with
      b64_json. Honoured now, and b64_mp3 is really encoded, because base64 of a wav
      under an mp3 key is a decode failure at the client.
      
      Also corrected in passing: versions.env pins UBUNTU_VERSION=24.04, whose
      system python IS 3.12, so the baked venv needs no bundled interpreter. The
      Dockerfile rewrote pyvenv.cfg at /opt/coderai/py312 unconditionally, which
      would have broken a working venv when nothing was bundled there, and the build
      refused outright without a relocatable 3.12 it did not need.
      
      Verified live after the fix: b64_wav returns 5,742,296 bytes (~15s of 48 kHz
      stereo), eviction by a 21 GB model stopped the worker and took the GPU from
      7958 MiB to 274 MiB in under four seconds, and ACE-Step then reloaded and
      generated again. Suite at its 15-failure baseline, 5299 passed.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      4b223f7b
    • Stefy Lanza (nextime / spora )'s avatar
      write songs with a model the server cannot hold in its own interpreter · da82cb71
      Stefy Lanza (nextime / spora ) authored
      
      
      ACE-Step generates a whole track -- style prompt, sung lyrics, a length in
      seconds -- and it ships a package pinning transformers 4.50 and spacy 3.8.4.
      spacy 3.8.4 publishes no cp313 wheel, so this is not a stack the server would
      rather not hold: it is one that cannot be installed on the server's
      interpreter. It gets a python 3.12 venv of its own and an HTTP service in it,
      driven the way CrisperWhisper, Canary and pyannote are. Built on the first
      request to such a model, so a model that is configured and never called costs
      nothing; `uv venv` covers the hosts that have python3.12 without ensurepip.
      
      Three upstream details that fail silently rather than loudly:
      
      * the pipeline resolves its own dtype as `bfloat16 if dtype == "bfloat16" else
        float32`, so asking it for float16 quietly doubles the VRAM. ACE_PIPELINE_DTYPE
        is read after that switch, so precision goes through there instead.
      * constructing the pipeline loads NOTHING -- it reads the checkpoints on the
        first call. coderai measures a model's footprint as the free-VRAM delta
        across the load, and that window closes when the worker reports healthy, so
        a lazy load would measure ~0 GB and eviction would treat a resident 3.5B
        model as free. The service loads before answering /health.
      * `quantized=True` does not quantize: it loads a separate int4wo export from
        the same directory layout. Missing, that is a FileNotFoundError minutes into
        a load, so the worker names the file it wants first.
      
      The handle cached in the manager exposes cleanup(), which is what eviction
      calls, and here it stops the worker -- the only thing that frees its VRAM.
      
      `duration` and `cfg_coef` are shared with MusicGen and default to 10s/3.0,
      which would make every ACE-Step call a 10-second clip at a guidance it is not
      tuned for. The branch reads which fields the client actually SENT, so an
      omitted one gets ACE-Step's default and an explicit `duration: 10` still means
      10. `lyrics` and `steps` become declared fields: both branches already read
      `steps` off the request, where only extra="allow" was letting it through.
      
      The venv is ~6.4 GB, so the default image does not carry it and --with-acestep
      bakes it in -- copying the dev venv when there is one, building it when there
      is not. Ubuntu 22.04's system python is 3.10, so a baked 3.12 venv also needs
      a relocatable interpreter bundled beside it, like the 3.10 venvs.
      
      Verified on a 3090: healthy after ~3.5 min, 8.5 GB VRAM, a real 48 kHz stereo
      WAV from a 10s/12-step request. Suite at its 15-failure baseline, 5292 passed.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      da82cb71
    • Stefy Lanza (nextime / spora )'s avatar
      give two models sharing a weights path their own cache slot · b7b32ea4
      Stefy Lanza (nextime / spora ) authored
      
      
      The silent one. self.models is keyed by model_key, and model_key was the
      weights path -- so two entries naming the same path shared ONE resident
      pipeline. LTX-2.3-PinkCherry requested while LTX-2.3-10Eros was resident never
      loaded its own transformer: no load, no eviction, straight to generation, and
      HTTP 200 with 10Eros weights. Every other instance of this family failed
      loudly; this one answered successfully with the wrong model.
      
      It also meant both wrote measured_vram_gb to the same key, so a Q4_K_M and a
      Q5_K_M of different sizes were overwriting each other sizing.
      
      A key is split only when the caller named something, two or more DISTINCT
      aliases point at that same path, and the named one is among them. A lone
      aliased model, or a request that names the path, keeps the key it has always
      had.
      
      That narrowness is the point, not caution for its own sake: collapsing every
      name form onto one key is deliberate a few lines above -- without it two
      concurrent first-requests under different spellings each load an instance and
      exceed max_model_instances=1. This is not a licence to key by whatever the
      caller typed.
      
      Verified live: the estimator now reports key=video:LTX-2.3-10Eros where both
      models previously shared key=video:diffusers/LTX-2.3-Diffusers.
      
      Suite: 15 failed, 5268 passed -- the same 15 as the baseline.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      b7b32ea4
    • Stefy Lanza (nextime / spora )'s avatar
      a masked attention pipeline gets the default backend, not the other fast one · 5aabd2fc
      Stefy Lanza (nextime / spora ) authored
      
      
      LTX-2 masks its text conditioning. The model loaded cleanly and died at the
      first denoising step:
      
          ValueError: `attn_mask` is not supported for sage attention
      
      so `auto` choosing sage there is not a slower path, it is a broken one.
      
      The first version of this guard moved it to flash instead, and it failed
      again with the same sentence and a different name:
      
          ValueError: `attn_mask` is not supported for flash-attn 2.
      
      NEITHER fast backend accepts a mask, so falling back from one to the other is
      not a fix. For these families the only working backend is the default SDPA,
      and the guard now keeps it.
      
      An operator who names sage or flash explicitly is still obeyed -- the override
      exists so they can force a backend, and silently ignoring it would be a
      different kind of wrong from silently choosing a broken one. They get the
      error, which is theirs to get.
      
      Wan and the LTX 0.9 family are unaffected and still take the fast backends;
      tests pin both directions.
      
      Suite: 15 failed, 5258 passed -- the same 15 as the baseline.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      5aabd2fc
    • Stefy Lanza (nextime / spora )'s avatar
      stop two models sharing a weights path from overwriting each other · 3bd589ad
      Stefy Lanza (nextime / spora ) authored
      
      
      A request for LTX-2.3-10Eros loaded LTX-2.3-PinkCherry: a different 15.9 GB
      transformer, silently, and from an entry that was DISABLED.
      
      self.config is keyed <kind>:<model_name> where model_name is the weights path,
      so entries sharing a path clobber one another and only the last survives. That
      is not a corner case -- it is how one base checkpoint is served under several
      configurations: lisa / lisa-32k differ only in context length, 10Eros /
      PinkCherry only in which GGUF is their transformer. PinkCherry comes second in
      models.json, so it won, parked or not.
      
      The VRAM estimator already disambiguated this way, by the id the caller asked
      for, after lisa-32k was once sized at lisa 51.97 GB. The config used to LOAD
      the model did not. Now both do:
      
      * every type setter stores a config under its ALIAS as well as its path, so a
        sibling is reachable by the only name that distinguishes it. The path key
        keeps its old meaning (last writer wins) for callers that know only a path.
      * _config_for_model_key takes the requested id and prefers the entry whose
        alias matches.
      
      Applied to all eight setters rather than video alone -- the collision is the
      same for audio, image and embedding, and fixing one type is how this survived
      being found twice already.
      
      Also: build_gguf_pipeline_components passed `model_name` as the diffusers repo
      supplying the component configs. Once siblings became reachable by alias that
      can BE the alias -- "LTX-2.3-10Eros", not a repo -- and from_single_file
      rejects it. The repo is whatever the entry names in `path`, every time.
      
      Verified live: LTX-2.3-10Eros now loads its own transformer and the pipeline
      assembles, 8/8 components.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      3bd589ad
    • Stefy Lanza (nextime / spora )'s avatar
      reconcile a converter and a class that disagree about twelve names · 720498e8
      Stefy Lanza (nextime / spora ) authored
      
      
      The LTX-2.3 GGUFs failed with "Cannot copy out of meta tensor; no data!", and
      the obvious reading -- parameters the checkpoint lacks -- was wrong.
      
      diffusers 0.38 converts an LTX-2 checkpoint prompt-adaln weights to
      prompt_adaln_single.* and audio_prompt_adaln_single.*, while
      LTX2VideoTransformer3DModel expects prompt_adaln.* and audio_prompt_adaln.*.
      Twelve names, one-to-one, the same tensors. They were present all along, under
      a name nobody reconciled, so they never materialised and stayed on meta.
      
      Measured identically on LTX-2.3-10Eros v1.5 and PinkCherry v1.8: 4444 GGUF
      tensors, 4186 expected parameters, and after the rename 0 missing and 0
      unused on both.
      
      The tempting fix was to initialise those twelve. That would have invented
      weights to paper over a rename -- precisely what the surrounding refusal
      exists to stop -- so it is NOT what this does. A key is moved only when the
      class does not want its current name AND does want the renamed one AND nothing
      already occupies the target, which makes it a rename of a tensor the
      checkpoint has and never the creation of one it lacks. A test pins that: a
      genuinely absent parameter must stay absent and the load must still fail.
      
      The converter is a third-party global, so it is wrapped for the duration of
      one from_single_file call and restored in a finally.
      
      Version-robust: 9 pass against the image diffusers 0.38, including the real
      checkpoint; against 0.37 the class has no prompt_adaln modules at all, so the
      repair is correctly a no-op and that test skips rather than asserting a fact
      about someone else is version.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      720498e8
    • Stefy Lanza (nextime / spora )'s avatar
      name the parameters a GGUF did not provide · 7790c253
      Stefy Lanza (nextime / spora ) authored
      
      
      "Cannot copy out of meta tensor; no data!" means some parameters were never
      materialised, and it names none of them -- which sends the reader hunting for
      a corrupt file.
      
      Measured on LTX-2.3-10Eros and -PinkCherry: diffusers already ships
      convert_ltx2_transformer_to_diffusers and it works almost completely, mapping
      their 4444 GGUF tensors onto exactly the 4186 the class expects. Precisely 12
      remain unprovided, all in one conditioning branch, identically in both
      fine-tunes. So the checkpoints are fine and the converter is fine; those
      fine-tunes were exported without that branch, and the 12 stay on the meta
      device.
      
      The refusal added earlier now carries that analysis: how many tensors mapped,
      how many remain, and which submodules they belong to. Best effort throughout
      -- any failure to analyse returns nothing and the original error stands.
      
      It deliberately does NOT offer to fill them in. Supplying parameters a
      checkpoint does not contain is a substitution, and the surrounding refusal
      exists precisely because a silent substitution (here: the 38 GB bf16
      transformer, on a 24 GB card) is worse than a failure. A test pins that: the
      hint must not grow a flag that papers over it.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      7790c253
    • Stefy Lanza (nextime / spora )'s avatar
      load a model that was added to the config while the server was running · 726daef7
      Stefy Lanza (nextime / spora ) authored
      /v1/models re-reads models.json on every call, but the loader's alias map is
      built ONCE at startup. So an entry added while the server runs is advertised
      immediately and cannot be loaded at all: the alias resolves to nothing, the
      raw alias reaches diffusers as a repo id, and the caller gets
      
          Repository Not Found for url https://huggingface.co/api/models/<alias
      
      >
      
      -- a 401 against HuggingFace for a model sitting on the local disk.
      
      Measured today registering Wan2.2-TI2V-5B: weights cached, `path` correct,
      and the request failed that way; the SAME model requested by its path loaded
      immediately, which is what pinned the cause to the alias map rather than the
      entry or the weights. The pre-existing Z-Image alias, updated rather than
      added, worked throughout -- that contrast is the whole diagnosis.
      
      This is the fifth instance of one pattern in this codebase: a name in
      models.json not matching the name the runtime registered. v0.5.4 read a
      backend from the wrong map, v0.5.5 looked for a binary where it does not live,
      v0.5.7 published an id that resolved to nothing, the LTX pair shared one path
      between two entries, and now the listing advertises what the loader cannot
      resolve. A cross-cutting identity-resolution audit is still the right fix; this
      is the narrow one.
      
      An alias the map has not seen is now looked up in the config itself and cached.
      A parked entry is deliberately NOT resolved -- parking is how an entry waits on
      a blocker, and resolving it would hand clients a model that answers 500.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      726daef7
    • Stefy Lanza (nextime / spora )'s avatar
      return the bytes when b64_json asks for them · b2da096f
      Stefy Lanza (nextime / spora ) authored
      
      
      Found by generating a real image against the live server: a request with
      "response_format": "b64_json" came back {url, path} and no bytes.
      
      The request model documents OpenAI's values -- url and b64_json -- and every
      OpenAI SDK sends the latter. save_image_response compared against this
      codebase's own older internal spelling, "base64", so with --file-path
      configured the documented behaviour ("if the client explicitly requested
      base64, include it") could never fire for a standards-compliant client. It got
      a URL, no bytes, and nothing in the response to say why. Nine call sites were
      affected: generations, edits, inpaint, upscale, depth, segment and the rest.
      
      Both spellings are accepted now. The url path is unchanged -- the point of
      --file-path is not to ship megabytes inline.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      b2da096f
    • Stefy Lanza (nextime / spora )'s avatar
      say which face_match profile was unusable, and why · 1d4728cd
      Stefy Lanza (nextime / spora ) authored
      
      
      Found by running the endpoint against the live server. A request naming
      `face_match.character_profiles` came back
      
          400: face_match was requested with no reference images
      
      which is the opposite of the truth -- the caller HAD supplied references. The
      cause was a bare `except Exception: pass` around the per-profile lookup, so a
      misspelt name, a profile with no saved images, and a characters directory the
      server resolves elsewhere all collapsed into that one message with nothing to
      distinguish them. Establishing which it was took a long sequence of probes
      against a running server: the field is declared (a bad type 422s with
      loc body.face_match.character_profiles), the profile exists under both
      candidate paths in the container, and resolve_character_profiles returns its
      four images when called in-process -- so the swallowed exception was the only
      thing standing between the symptom and the cause, and it is the defect.
      
      Now each profile is resolved separately and its failure is named. When none
      resolve the 400 lists them; when some do, the request proceeds (there ARE
      references to match against) and the unusable ones are logged, because a
      performer the caller believes is enrolled and is not would make a genuine face
      read as "matches no consenting performer" for the wrong reason.
      
      Verified live on the same server, all four checks against a real face:
      nsfw 0.0003, one face at det_score 0.879, age 38.0 with the interval 30.0-46.0
      and minor_probability 0.0, face_match similarity 1.0 matched against itself,
      unmatched_faces 0, flagged false.
      
      Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
      1d4728cd
Loading