- 28 Oct, 2026 20 commits
-
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
13 (per-request progress), 13b (encode frames + the job log), A4 (72s -> 21.4s per still), plus models.civitai_cache_dir. B2, B3 and B4 were already implemented and are verified live. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
The whole suite green: 5507 passed, 0 failed. Two real defects came out of it (a refusal that named neither the capability nor the fix, and a PackageNotFoundError where a missing bitsandbytes should be reported), the rest were expectations left behind by changes to the product. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Section 14 of the Motus list: 14a (AI music never reached compose), 14c (wrap text instead of shrinking it below readable), 14d (a clip's sound from its own file). 14b was fixed in 0.5.12 by making an image load visible to the eviction ladder. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Four fixes found from one question about two stray ffmpeg processes: - an audio track is built at its length instead of padded up to it, so the mix cannot hang at 100% CPU (4/10) or silently come out a clip short (3/14) - an ffmpeg we start can no longer outlive us (PR_SET_PDEATHSIG) - an image load reserves its VRAM for its load window, so it is visible to the eviction ladder instead of handing out memory it has already taken - the dtype default is chosen from a device that has been detected, so a text model stops loading fp32 on a CUDA card -- twice the VRAM, and no Flash Attention, which is why Qwen3.5-9B answered nothing since 2026-09-30 Plus `precision` is settable from the model page, where the quantization it belongs with already was. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
`precision` was read by the loader and listed in main.py's _LOAD_AFFECTING, but it was in neither the admin save allowlist nor the model page, so the one setting that decides how much VRAM a card actually needs could not be configured anywhere. The dtype then came from a default derived from GPU detection -- and a card's precision is a property of the card, not of the hardware it happens to land on. The select sits with the quantization boxes because that is where it is read wrongly: 4-bit and 8-bit quantize the WEIGHTS, while the embeddings, lm_head, the norms, the activations and the KV cache all stay in this dtype, so a 4-bit model left in fp32 still pays fp32 for every one of them. The hint says so, and says that fp32 also rules out Flash Attention. "" is stored as null, so "not configured" stays distinguishable from a value and the loader's own default still applies. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
place_at padded a mix up to the scene length with apad and let atrim end the stream. That handoff is a race. A bare apad loses it by generating silence for ever: ffmpeg never reaches EOF and spins at 100% CPU with the output half-written -- 4 hangs in 10 runs, and two orphans left on a dev host by test_compose_dialogue had burned 14h and 10h of CPU unnoticed. apad=whole_dur stops the hang and loses the same race the other way, emitting a track one clip short (3s for 4s) in 3 of 14 runs with no error anywhere. Neither -threads 1 nor -filter_threads 1 helps. A silent base input of exactly the target length, mixed under the placements with amix=duration=first, has no handoff to lose: 0 bad in 20 runs, one output size. The other apad sites feed no mix, but take whole_dur too since the length is known there as well. Also pass config/subfolder to the single-file component loader, the way the GGUF branch already does and for the same reason: left to itself from_single_file infers a base repo from the checkpoint's own metadata, so Krea-2-RealDream reached for krea/Krea-2-Raw and died on a repo nobody named. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
- 27 Oct, 2026 20 commits
-
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
The OCI image is a capture of a virtualenv: whatever has fallen out of the venv is gone from every install that pulls the image, and nothing in the build log says so. `gguf` left this venv at some point, and the rebuild duly shipped without it -- which breaks every GGUF component load with Unable to load weights from checkpoint file for '<file>.gguf' because diffusers takes the .gguf branch, fails to `import gguf`, and that ImportError is swallowed into a generic OSError about the file. Nothing in the message mentions a package. The baseline is the PREVIOUS IMAGE, not requirements.txt. That file lists optional extras this venv has never had -- vosk, timm, edge-tts and ten more -- so checking against it reports thirteen false alarms and teaches everyone to pass --no-req-check. What the last image shipped is what this install has today, and losing any of it is a regression by definition. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Krea-2-Turbo and LTX-2.5 have sat disabled with the same note: the classes they name -- Krea2Pipeline, LTX2DurationHead, LTX2VideoDiffusionDecoderModel -- did not exist in diffusers 0.38. They are in 0.42.1, a release rather than git main, so the floor moves. The pin that matters is the one next to it. diffusers 0.42 wants huggingface_hub >=1.32, transformers 5.x refuses >=2.0, and pip resolves 2.3.0 given the chance -- which leaves transformers unimportable and every model with it. The usable window is 1.33 and it is written down here rather than rediscovered. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Plan only -- nothing built. Three things it had to establish rather than assume: * the pod cores are python:3.12-slim, which is exactly what ACE-Step needs. The bundled standalone interpreter the local image requires (Ubuntu 22.04's system python is 3.10) is simply not a problem on a pod. * ACE-Step cannot go in the `audio` profile: that one has no `.light` marker, so it builds on the FULL core, and 7 GB of unused CUDA plus a 6.4 GB venv is far over the measured ceiling (10.1 GB boots, 14.5 GB is refused). * the `gguf` package is in requirements.txt but in NEITHER core.txt nor video.txt, and pod images install core.txt -- so no pod image can load an LTX-2.3 single file today. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-