- 27 Oct, 2026 24 commits
-
-
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
-
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:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
Stefy Lanza (nextime / spora ) authored
LatentSync (Apache-2.0) and MuseTalk (MIT) are now first-class presenter engines and come before wav2lip for a clip, so the compose documentation says which engine engine:"auto" will pick and why: wav2lip works at 96x96 and upscales, which is where the soft mouth on a 1080p reel comes from, while these two run at 256-512 px natively. sadtalker still leads for a still portrait because it is the only one that invents head motion rather than replacing a mouth. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
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:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
-
Stefy Lanza (nextime / spora ) authored
9ae240b0 committed its tests and its documentation and none of its implementation: four files instead of twelve. The `git add` that should have staged the rest named a pathspec for requirements-acestep.txt, which did not exist, so git refused the whole invocation and I did not check its output before committing. The message described work that was not in the tree, and the suite at that commit could not have passed. This is that work: * video.py — LTX-2.x gets its own pipeline classes. This diffusers has LTX2Pipeline, LTX2ImageToVideoPipeline, a video AND an audio VAE, but only the LTX 0.9 family was mapped, so every LTX-2 entry in models.json sat parked: the name matched and the class did not. Split on the version in the name, since both families are just called "LTX" and a fine-tune keeps its base version (10Eros is LTX2.3, PinkCherry is LTX23). * hf_loading.py — `civitai:<modelVersionId>` references, and a GGUF component that fails to load now fails the load instead of being skipped. A skipped component is simply absent from the dict, so the base pipeline loaded its own full-precision copy: the operator asked for a 14 GB quantized transformer and got a 38 GB bf16 one, while the VRAM estimate still said 14 GB and the planner therefore freed about a third of what the load needed. * presenter.py — LatentSync and MuseTalk as first-class engines, ahead of wav2lip for a clip (wav2lip works at 96x96 and upscales, which is the soft mouth on a 1080p reel). Both are video-driven, so sadtalker still leads for a still portrait. Recognised from a shim on PATH or a managed install whose weights are already present — never "could be downloaded", because a compose job is the wrong place to fetch 5 GB. * cache/__init__.py + app.py — the loop stall. scan_cache_dir is not a listing: it stats every blob of every revision of every repo, measured at 27.3 SECONDS on this machine's 900 GB cache, and it was called from a per-model VRAM estimate, a GGUF path lookup and a POLLED endpoint, all on the event loop. 6631 stalls over 0.5s, worst 13.0s, serving nothing. Both halves were needed: memoised for 30s (short enough that a model downloaded while the server runs still becomes visible) and the poll moved to asyncio.to_thread, since memoising alone still puts the first 27-second call on the loop. * manager.py — /v1/models reads `modes` from a video entry's long-standing `video_subtypes` as well as the newer spellings. * requirements-acestep.txt — the file whose absence caused this. It is marked NOT YET INSTALLED, because it is: the install was refused as external code, nothing in it has been run, and the audio_gen loader refuses an ACE-Step model explicitly rather than mis-loading it as a MusicGen. Suite: 15 failed, 5194 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
serve the LTX-2 family, fetch a Civitai checkpoint, and stop walking 900 GB of cache on the event loop Model installation work from the client's C list, plus one latency bug found on the way. LTX-2 is wired. This diffusers has LTX2Pipeline, LTX2ImageToVideoPipeline, a video AND an audio VAE — but video.py only mapped the LTX 0.9 family, so every LTX-2 entry in models.json sat parked: the name matched and the class did not. Split on the version in the name, since both families are just called "LTX" and a fine-tune keeps its base version (10Eros is LTX2.3, PinkCherry is LTX23). Stable Audio had its own entry in the type map pointing at AudioLDM2Pipeline — a different architecture with a different projection model and sample rate, so a configured stable-audio-open entry could only ever fail at load. It has its own loader now, keeps the stereo field (folding it to mono throws away the point of the model) and clips before the int16 cast, because a float above 1.0 wraps to a loud negative sample and is heard as a click. ACE-Step gets requirements-acestep.txt and an explicit refusal naming what it needs, rather than being loaded as a MusicGen and failing with a shape error three minutes later. It ships its own package pinning a stack this venv cannot hold, so it needs an isolated-venv worker like CrisperWhisper and Canary; that worker does not exist yet and the refusal says so. A GGUF component that fails to load now fails the load. It was skipped with a log line — and a skipped component is simply absent from the dict, so the base pipeline loaded its own full-precision copy: the operator asked for a 14 GB quantized transformer and got a 38 GB bf16 one, while the VRAM estimate still said 14 GB and the planner therefore freed about a third of what the load needed. A silent downgrade to something that does not fit is worse than a refusal. `civitai:<modelVersionId>` references, because a large part of the community's fine-tunes are published only there, as one ComfyUI-style checkpoint — exactly the shape build_single_file_pipeline already handles. The only thing missing was a way to fetch one. The VERSION id, not the model id: a Civitai "model" is a family, and an entry naming the family would silently change underneath the operator on a new release. Content type and size are both checked, because an unauthenticated download answers with a web page at HTTP 200 and a naive fetch saves it under a .safetensors name — which surfaces hours later as an unreadable tensor header. LatentSync and MuseTalk are first-class presenter engines, ahead of wav2lip for a clip: wav2lip works at 96x96 and upscales, which is where the soft mouth on a 1080p reel comes from. Both are video-driven, so sadtalker still leads for a still portrait — it is the one that invents head motion. Recognised from a shim on PATH or a managed install whose weights are already present, never "could be downloaded": a compose job is the wrong place to fetch 5 GB. And the loop stall. huggingface_hub.scan_cache_dir is not a listing: it stats every blob of every revision of every repo, measured at 27.3 SECONDS on this machine's 899 GB cache. It was called from a per-model VRAM estimate, a GGUF path lookup, and app.internal_engine_state — which is POLLED — all synchronously on the event loop. The watchdog's verdict was 6631 stalls over 0.5s with a worst of 13.0s, during every one of which no request of any kind was served. Fixed with both halves, because either alone is insufficient: the scan is memoised for 30s (short, so a model downloaded while the server runs still becomes visible) and the engine-state poll runs it through asyncio.to_thread, since memoising alone still puts the first 27-second call on the loop. Also: /v1/models reads `modes` from a video entry's long-standing `video_subtypes` as well as the newer spellings. Suite: 15 failed, 5194 passed — the same 15 as the baseline. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
normalise a language before transcribing, stop inflating weights that are already quantized, and never answer 200 with nothing (v0.5.9) Five bugs and five features from a client report. The bugs share a shape: the server held a belief about a model that nothing checked against the model. A1. A TTS voice is chosen for an accent, so the language a compose render carries is "en-us" / "pt-br". Passed to transformers' Whisper -- which accepts only the English NAME -- it raised "Unsupported language: en-us" and 500'd every captioned scene of every render. Reproduced verbatim against the running server before the fix. New codai/api/lang_codes.py normalises once, per backend: the NAME for the transformers pipeline, the ISO code for whisper.cpp, Canary and faster-whisper, and None (auto-detect) for a tag that maps to nothing -- refusing to transcribe because the caller named a dialect is never the better outcome. Language VALIDATION now compares by meaning, so a model declaring "english" no longer refuses "en-gb", and compose's model picker stops disqualifying the only model that can time words. A3. FLUX.2 (dev/klein/pro) is a unified generate-and-edit model whose name carries neither "edit" nor "kontext", so it fell through to the generation branch and advertised plain img2img -- telling clients to show a `strength` slider for a pipeline that has no such argument. Confirmed live: ['image_generation','image_to_image','inpainting']. diffusers 0.37 agrees it is an instruction edit (Flux2Pipeline takes `image`, not `strength`), which is what the runtime already decided on; only the advertised list disagreed. A4. The estimator normalises a disk size to the load precision. A GGUF was special-cased to 1.0 because its bytes ARE its footprint; an HF checkpoint published pre-quantized is the same case by another route and was not. Z-Image-Turbo-unsloth-bnb-4bit: 16.3 GB of 4-bit weights, 1.70x factor, estimated 30.03 GB against a 24 GB card. Nothing can free 30 of 24, so every still ran partly on the CPU -- ~72 s for one image from a few-step turbo model. A5. Qwen3.5-9B answered content "" with HTTP 200 for four weeks. All three rungs of the generation ladder raised ("Pointer argument (at 0) cannot be accessed from Triton (cpu tensor?)") and the last one assigned generated_text = "" and returned normally. Now raises. The Triton error is a quantized model whose weights were CPU-offloaded: such a load succeeds and then fails every request, so it is refused at load time and a quantized model tries full-GPU whatever the estimate says. That also breaks a loop -- measured_vram_gb = GPU + offloaded + reserve means one offloaded load writes a number bigger than the card, and the next load reads it and offloads again. B1. A scene can carry its clip's own sound: visual.keep_audio, scene.audio_mix (voice|clip|both) and clip_volume. "clip" makes the scene as long as the clip and skips the TTS call; "both" ducks the clip under the voice. Folded into the narration track, so the music bed ducks under it like a voice. B2. Two characters talking: scene.speakers, each line in its own voice, each character lip-synced in their own column (or the frame cutting to whoever speaks). One sync run per CHARACTER, not per line -- each gets a track holding only their own lines, so the mouth moves on their words and stays closed while the other talks. Captions never mix two speakers and carry the attribution into the .srt/.vtt. B3. characters: ["alice","bob"] on images and video. The gap was never the field: IP-Adapter conditions on the MEAN of its references, so a second character's pictures produced one person who looked like neither. Reference sets are kept apart and handed to a model that takes several (FLUX.2, Qwen-Image-Edit); a model that cannot do it says so instead of blending. B4. POST /v1/moderation -- nsfw, faces, age, face_match over images or sampled video frames. It fails CLOSED: a missing model is a 503 naming it, not a result with the check quietly absent, because a caller cannot tell a missing check from a passed one. minor_detected is nullable (explicit content with no face cannot be cleared on age) and keys on the LOW end of an age interval, not a point estimate from a model with a multi-year error. C5. Per-model defaults (steps, guidance, sampler) that fill in only what the caller omitted, published in /v1/models alongside modes and licence -- so a client stops keeping a table keyed on the model NAME and an 8-step model stops being run for 30. A sampler switcher that refuses to cross the flow-matching boundary (a FLUX-family pipeline handed a DDIM produces noise, not a worse picture). Stable Audio gets its own pipeline: it was mapped to AudioLDM2, so a configured entry could only fail at load. Two of my own regressions, caught by the suite's guard tests and fixed here: /v1/moderation was unmapped in the remote gateway, and the moderation availability probe LOADED each model -- which turned the /v1/models listing into a 350 MB download on the event loop. Suite: 15 failed, 5146 passed -- the same 15 as the baseline. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
- 26 Oct, 2026 7 commits
-
-
Stefy Lanza (nextime / spora ) authored
v0.5.6 registered operator fonts by dropping a snippet in $XDG_CONFIG_HOME/fontconfig/conf.d. On a host install that is right. In the container it is not: the launcher points XDG_CONFIG_HOME at the config VOLUME ROOT, which an unprivileged process cannot create a directory in, so the first restart after shipping it said [fonts] could not register ['/home/ubuntu/.coderai/fonts']: [Errno 13] Permission denied: '/config/fontconfig' and captions kept using the substitute. Two faults, not one: that path is not writable, and it is also inside the container's own filesystem, which a --rm container discards at every restart -- so even where it worked it had to be rewritten on each boot and could never be relied upon. The snippet is still tried first, because fontconfig reads it unprompted and so it survives into a grandchild. When it cannot be written, coderai now writes its own fonts.conf into the config dir one level down -- writable, and bind-mounted, so it persists -- and exports FONTCONFIG_FILE, which every child this process starts inherits. That file <include>s /etc/fonts/fonts.conf, so replacing the main configuration costs nothing: the system families still resolve. Verified in the container under the real environment, with the check that actually matters -- fc-match in a CHILD process, which is the lookup ffmpeg/libass performs: child fc-match 'Montserrat:style=Bold' -> Montserrat fc-match 'DejaVu Sans' -> DejaVu Sans (44 faces) The bug got through because it was verified with `docker exec`, which inherits a different environment than the supervised server process. A font this process can find is not necessarily a font the renderer can use, and an exec is not the deployment. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
A whisper-server model could not be used by the id /v1/models advertises for it. Asking for it reported the weights as a missing HuggingFace repository: 500 Transcription error: whisper-large-v3-turbo-q8_0.bin is not a local folder and is not a valid model identifier listed on 'https://huggingface.co/models' which reads like a broken download and is nothing of the kind. One whisper-server is described by TWO models.json rows: a RUNNER, carrying model_path + server_path + port, registered under its id (whisper0..whisper3 here); and a MODEL row naming the weights, which registration skips on purpose as "the model config, not a runner". But /v1/models takes its id from the model row -- so the published name was the one name that matched nothing: not a runner key, not an alias. The lookup missed, and the request fell through to the generic audio path, which can only interpret a string it does not recognise as a hub repo id. Now every name that identifies the same runner resolves to it: its own id, an alias (round-robin as before), and the weights by full path, basename, or basename without extension. Where several runners share one file, naming the file prefers an idle one, so it balances the way an alias does instead of always landing on whichever registered first. This is the third defect in one week with the same shape -- a name in models.json not matching the name the runtime registered. v0.5.4 read a backend from the runtime map instead of the file that declares it; v0.5.5 looked for a binary in a directory that does not exist; this one publishes an id it cannot resolve. The pattern is worth an audit of its own. Also pins a rule on the VRAM estimator that it already followed in most places and now cannot lose: an estimate of zero means NOTHING WAS MEASURED, never "measured as needing nothing". A stored zero was already read as absent; v0.5.6 stopped a configured zero from claiming authority over a real measurement; the two tests added here also cover a zero arriving with force_vram_update, which is the one flag that makes a measurement beat the operator's own number. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
fit the caption to the frame, install the font that draws it, and stop reading an unset field as a free GPU (v0.5.6) Four defects behind one report: vertical renders came back with caption lines running off both edges, in the wrong typeface, and speech-to-text answered 500/503 while a text model held the card. THE FONT WAS NEVER IN THE IMAGE. Dockerfile.oci and Dockerfile.update install fonts-montserrat; Dockerfile.oci-venv -- the one build_oci_image.sh actually builds -- installed no fonts at all, so no image built the normal way has ever had the family the compose API documents and defaults to. libass does not fail on a missing family, it substitutes: DejaVu is considerably wider, which is how a missing package became a layout bug with nothing but a log line to say so. All three Dockerfiles now install the fonts AND fontconfig (Dockerfile.oci had the fonts but not fc-match, so it could not even tell whether a family existed). And because an image rebuild is a poor way to add a brand font, fonts can now be supplied per installation: files dropped in <config>/fonts -- or wherever CODERAI_FONT_DIR points -- are registered at startup by writing a fontconfig snippet into the user config. The snippet matters more than it looks: captions are burned by libass inside an ffmpeg CHILD process, which does its own fontconfig lookup, so a font this process can find is not necessarily a font the renderer can use. The config volume outlives both a restart and a new image, which is the point. CAPTION SIZE CAME FROM THE FRAME HEIGHT. On a vertical video the height is the LONG side: at 1080x1920 the font was sized 0.052 * 1920 ~ 100px to be drawn across a 1080px frame. It is now sized from the short side, which leaves 16:9 byte-identical (there the short side IS the height) and gives 9:16 the same text-to-width ratio as 16:9. Line breaking upstream works on a word budget and cannot know how wide the words are, and WrapStyle is 2 -- no automatic wrapping -- so a line that still overflows was simply drawn past the edge. Each event's widest line is now measured against the real font file, and one that does not fit gets its own smaller size through an inline \fs override: the long line shrinks, not the whole video. Measured with the actual face rather than estimated per character, because the families involved differ in width by exactly the margin that decides the question. Clients scaling font_scale down by hand for 9:16 and 4:5 can stop. AN UNSET FIELD READ AS A FREE GPU. used_vram_gb=0 was honoured as authoritative -- yet zero is this estimator's own "could not determine" sentinel, and it is what the admin UI stores for a field the operator never filled in. A 27B Q4_K_P with used_vram_gb=0 and measured_vram_gb=18.0 therefore estimated 0.00 GB against 8852 MiB of free VRAM. Nothing was evicted, every layer was aimed at the GPU, the load failed and retried down a ladder -- all, 52, 39, 26 layers, re-reading the weights at each rung -- for 11m50s, and settled with 39 of 66 layers on the CPU. The request then generated for another 19 minutes, 31m04s in all, holding GPU ownership the whole time: every other model queued behind it, which is what made speech-to-text answer 500/503. The swap gate was working correctly; it swapped the moment the owner finished. A real measurement is better information than an unfilled field, so it now wins. An operator who means "no weights on the GPU" has n_gpu_layers=0, which is handled separately and still reserves nothing. Worth recording for the next time this looks like a quantization problem: it was not. Those weights are 4-bit on disk and the KV cache was already q4_0/q4_0 with flash attention -- about 2.2 GB for n_ctx=32000. Both were at their most aggressive setting while the model sat on the CPU. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
The DINOv2 GGUF embedder answered 500 for about twelve hours -- 6,176 image embedding requests -- for two independent reasons, and the capability can be dropped again by an ordinary rebuild, so both the lookup and the packaging are fixed here. The resolver looked only under /opt/coderai/bin, a directory that does not exist in the image, and reported its absence as a generic load failure. It now tries DINOV2_EMBED_BIN, then PATH, then the places the binary is actually installed, and when it finds nothing it names every one of them plus the build script -- because there is no fallback: llama.cpp cannot load a ViT-only arch, so this binary is the only runtime this model has. The binary was also never listed in the image build, having been compiled by hand once; the next rebuild silently shipped without it. It is listed now, along with the private shared libraries it cannot start without. Those libraries are the larger half of the problem. The image has no system OpenCV -- only the venv's cv2 wheel, a different version with mangled sonames -- so the closure travels beside the binary in dinov2-embed-libs, reached through its own RPATH rather than LD_LIBRARY_PATH: OpenCV placed in local-libs would be offered to every process in the image, cv2 included, while a private directory on one binary's RPATH cannot shadow anything. Four things make that work, each learned by watching it fail: - the whole dependency tail, not just libopencv_*. Debian's imgcodecs links GDAL, GDCM and OpenEXR, which a JPEG decoder never calls and the loader demands anyway. Copying three libraries left six still "not found". - cp -L. Most of these paths are symlinks, and copying the link gives a 20-byte file -- which also made a first size estimate off by 7x. - RPATH on every library, not only on the binary. patchelf writes DT_RUNPATH, which unlike DT_RPATH is not inherited by a dependency's dependencies, so the binary found imgcodecs and imgcodecs found nothing. - everything above the base ABI, not only what the image lacks. libtiff.so.6 exists there, so it was skipped, and the newer GDAL then wanted LIBTIFF_4.6.1 from it. Mixing distributions works only if the whole tier comes from one of them. What stays behind is what any Linux has: libc, libm, libstdc++, libgcc, libgomp, pthread, dl, rt, the loader, and libvulkan, which must match the GPU driver. The tarball build had the same two gaps as the image build and is fixed with it: it copied a binary without its -libs sibling, shipping a program that cannot start, and a --local-binary-dir sweep would have flattened those .so files into bin/ as programs, away from the only place the RPATH looks. Verified against the HF vision backend on the same images: cosine 0.990-0.995 per image, with image-to-image geometry agreeing to 0.002-0.017 -- the quantized binary and the reference implementation describe the same space, so an index built by one stays valid under the other. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
Both of these were found by running the previous commit rather than trusting it, which is the only reason they are not still in. The audio backend was read from the manager's per-model config map. That map holds RUNTIME kwargs, and a whisper-server model has no row in it at all -- it lives in `whisper_servers` -- so the lookup answered "no backend" for exactly the models whose backend decides the question, and the pick that was supposed to avoid them chose `whisper2`. The entry is now read from models.json, where `backend` is actually written, matched on id, alias, path or basename because an audio model is referred to by any of them and the same alias appears twice in a real catalogue. Verified against this install's sixteen audio models: every one resolves, and the pick is now the model measured to return fourteen word timings rather than the one measured to return none. The new warning is what caught it: the render reported "'whisper2' returned no word timings" instead of quietly estimating. That is the whole point of it. And `enabled: false` was hidden behind `all_engines`, which is the wrong seam. The node-wide list the system worker serves -- the one the front hands clients -- MUST pass all_engines=True, because that worker has no engine of its own and would otherwise list nothing. So the filter was off on the only path that mattered, and /v1/models offered LTX-2.5, Krea-2-Turbo and Krea-2-Edit: three models configured ahead of a diffusers version that does not exist yet, each of which answers 500. `include_disabled` is now a separate question, asked only by the admin config view, because that is where a parked model gets re-enabled. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
Follow-up to v0.5.2, from measuring the fix instead of trusting it. Asking the running install for word timings on 4.3 seconds of Italian it had just synthesised showed two more faults in the same chain, both of which answer 200 with the timings missing. A CTC pipeline cannot take `generate_kwargs=None`: it iterates it and raises `TypeError: 'NoneType' object is not iterable`. The backend passed exactly that whenever a model had no generate kwargs, which is every wav2vec2/MMS checkpoint -- and the retry below it then turned the failure into a text-only success, so the only trace was one line in the log. `wav2vec2-it` returned 0 words before this and the same 14 the pipeline always produced after. The kwarg is now omitted when empty rather than passed as None. And the pick has to consider the language, not just the backend: a model that declares `languages` REFUSES the rest with a 400, so an install with `wav2vec2-it` and `wav2vec2-en` has two models that each report word timings and each reject the other's language. Picking one for the wrong language would have replaced one silent fallback with another. Measured, on sixteen configured audio models: ten cannot time words at all. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
Two faults seen on a long render, plus the image-side work from the same reports. Caption timings never worked. The render asks for word timestamps on the narration clip it just wrote, so it passes a NAME where the HTTP endpoint passes an upload -- and the transcription core read `.filename` off it unconditionally, raising `'str' object has no attribute 'filename'` on every scene of every render. The caller caught it and fell back to estimating from character counts, so captions drifted against the voice and nothing ever failed. `_upload_name` now takes a path, an upload or nothing; only the extension is read from it, so a name was always enough. Fixing that alone would have changed nothing visible. The model the renderer picked by default was chosen by NAME -- "whisper" first, then canary -- and both are the two backends that cannot time words: the whisper.cpp runner returns text with no word array, and the NeMo worker ignores the flag. The pick now asks which backends report them (the HF pipelines, vosk, faster-whisper), and a model that answers with none of them is named in the job's warnings instead of silently downgrading the captions. Polling a render cost a render. `GET /v1/video/compose/{id}` sits under the `/v1/video/` prefix, which allows 10 requests a minute per IP, so a client polling a four-minute render from the address it submits from spent the whole budget on the polls and got 429 on the work. Read-only job status -- compose and talking-head -- is now exempt from the limit, by prefix and for GET only, so the sibling POST .../cancel stays limited. It is NOT exempt from authentication: the document names the job's output, unlike the progress endpoints it sits beside. Also here, from the image-edit reports: a uniformly black or white result is refused with the dtype named rather than answered 200, `--no-ram` no longer coerces a configured bf16 to fp16 (same two bytes, a quarter of the range, NaN latents, black picture), single-file and GGUF checkpoints load through the configured loader instead of falling back to a different one, per-model LoRAs are an identity rather than a hint, and a repository may run its own pipeline code only from an allow-list that starts empty. And: the measurements stay, the customer names go -- shipped images are public. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
- 25 Oct, 2026 9 commits
-
-
Stefy Lanza (nextime / spora ) authored
Both found by reading the output of a real page through a real pod, which is the only place either was visible. WHICH CONFIGURATION. A pod answers with the engine it ran, because the configuration ids in ocr.engine_configs are an orchestrator-side naming layer it has never heard of. So `engine=surya-layout` came back `engine: surya`, same as plain surya -- and telling two configurations of one engine apart in the answer is most of the point of having two ids. The local path already reported the id; now both do, and a plain engine name still reports itself. A CONFIDENCE NOBODY MEASURED. surya's table recogniser does not score cells in the mode we call it in, so every cell carried `conf: 0.0` -- a placeholder wearing a measurement's clothes, which is the confusion `confidence_measured` already exists to prevent for pages. The key is now present only when surya reported a number, and the table says `confidence_measured` so a caller gating on cell confidence can tell whether there is anything to gate on. Presence decides it rather than truthiness, because a real 0.0 IS a measurement. Table markup likewise: `html` is absent rather than empty, so "this engine returns no HTML" (surya, which returns cells) is distinguishable from "this table's HTML is empty". Two of the tests written for this asserted against a fixed byte window after a `def`, and failed on the correct edit that grew the function -- the same brittleness fixed in test_image_edit_uses_an_image_pipeline an hour earlier. They read the function by AST now. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
The layout pass read its per-page option with `_opt("layout", True)`. The accessor is `_page_override`. A pod answered 500 {"detail": "surya ocr failed: name '_opt' is not defined"} after the image was built, pushed, signed and booted, because nothing executes these modules until a page reaches a pod: `codai/ocr/workers/*` run OUTSIDE the package -- the surya worker in its own venv, forbidden from importing codai -- so no test had ever run a line of them. The test I wrote for the layout pass asserted the SOURCE STRING of that call and proved only that the text was there. So the fix comes with a guard that would have caught it: every `_`-prefixed name a worker module loads must be bound somewhere in that module. Those are the module's own helpers, so an unbound one is a typo and not a global from elsewhere. Pure ast + builtins -- no linter, nothing added to requirements -- and it is checked against the bug itself, because a checker that passes everything is worse than none. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
`pod_ocr_engine` rewrote EVERY whole-request OCR forward to docTR. The premise was that a pod image cannot carry a VLM's model server, which was true of `coderai-vllm` and never true of `coderai-vllm-ocr`: that image carries surya, the rasteriser and its own vLLM -- it IS the pod-side pipeline -- and it carries no docTR at all, which is why its pods launch with CODERAI_OCR_FALLBACK_ENGINES=off. So the rewrite asked a pipeline pod for the one engine it cannot possibly have, and the pod answered "OCR engine 'doctr' is not enabled". The question cannot be asked without the image, and the gateway was not passing one. Found by sending a page through a pod, not by reading the code: the id resolved, the pool provisioned, the pod booted and went ready, and the request still came back 400 naming an engine nobody had asked for. A named configuration (ocr.engine_configs) also resolves to its base engine here. The ids are an orchestrator-side naming layer and a pod is a pre-configured instance of one -- its env already carries the overrides -- so telling it the id names an engine it has never heard of. Omitting the image keeps the old answer, so callers that cannot know it are unchanged. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
A model id is a configuration, not a set of weights. Image models have worked that way for a long time -- one checkpoint, several published ids, one copy in VRAM -- and five things that should have followed from it did not. PER-MODEL LoRA STACKS. A diffusion pipeline could carry an acceleration LoRA and per-request LoRAs, with nothing in between: no way to say "this id is that base plus this style LoRA at 0.8". `loras` on a model entry says it, in the shape text models already use. `lora_path` is honoured too -- it had been in the config and the admin page for years while nothing on the diffusers image path read it. And `set_adapters` replaces the ACTIVE SET, so the image path's request-LoRA call was switching the pipeline's own adapters off: a 4-step distilled preset running un-distilled, and an id published as a fine-tune serving its base checkpoint. 200 OK, a plausible picture, nothing in the logs. The video path had always re-included them. A per-model LoRA that will not load now FAILS THE LOAD. The acceleration path logs and continues because the cost there is speed; here it is identity. SINGLE-FILE CHECKPOINTS. `from_single_file` was wired up only for GGUF components, so the one .safetensors Civitai ships for every community fine-tune could not be opened at all. A model path may now name a file, or `repo:file.safetensors` -- community repos hold every version side by side, so the repo id alone does not say which to serve. A SAMPLER IS A CLASS PLUS ITS OPTIONS. "DPM++ 2M SDE Karras" is DPMSolverMultistepScheduler and two settings; naming the class alone gave plain DPM++ 2M, a different sampler, silently. `scheduler_config` carries them. RASTERISATION, PER REQUEST. A scan is normally handed to the model as the scanner wrote it -- 0.1 ms and 147 KB against a render plus ~1.8 MB of PNG -- but only between the target DPI and `page_image_max_dpi_factor` times it, and only install-wide. `native_dpi` removes the ceiling for one request; the floor stays, because a page passed through BELOW the target is a coarser page, which is what cost surya-ocr-2 its Italian accents once already. An uploaded image now carries its own bytes too: it was decoded and re-encoded to arrive at the picture the caller already had. SEVERAL IDS FOR ONE OCR ENGINE. `ocr.engine_configs` gives OCR what image models had: named configurations of one engine, each its own id, its own catalogue row, its own pool. An override naming a setting that does not exist is refused rather than quietly serving the base settings under another name. Which forced the last one into the open. `/v1/ocr/engines` advertised `surya: {regions: true, tables: true}` while the surya worker built every page with `"regions": [], "tables": []` hardcoded. surya ships a layout predictor and a table recogniser; they were never run. A client that chose surya BECAUSE the catalogue said it returned regions got empty arrays and no error. Both halves are now true by construction: the row is read from `surya_layout` / `surya_table_rec`, and those load the predictors. Regions come back with surya's own reading order (LayoutBox.position, explicit as `order`, -1 where an engine states none) and tables as cells with row/column ids. Loading is configured; spending is per request. `layout=true` chooses whether to pay for the pass on an engine that HOLDS the predictor -- it cannot make one acquire a model mid-page, which is how a footprint grows past what the VRAM accounting reserved. Asked for where none is loaded, it is refused with the configuration to use instead; answering 200 with empty regions would be the same lie wearing a request field. Pods differ from their siblings too: a pool whose entry names an `ocr_engine_config` boots pods with that configuration's settings, and a named config overriding `<engine>_model_id` reaches its own pods rather than sharing its sibling's. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
set_image_model keys config by path: self.config[f"image:{model_name}"] = config or {} so a second image_models entry for the same checkpoint overwrote the first and said nothing. Two configs of one checkpoint is not an odd thing to want -- the same base served with a different transformer, quantization or step count -- and text models have supported it for a long time, addressing the second under its alias and skipping an unaliased duplicate with a message. Image models had none of that. The symptom is worse than losing an entry, because nothing looks lost: both entries still appear in /v1/models and both register their aliases, so a request for EITHER is answered with whichever config was registered last. Found while testing two configs of Qwen-Image-Edit-2511, one with a GGUF transformer and one with a fine-tuned transformer quantized at load: a request for the GGUF config was served by the 4-bit one, and the only clue was "Registered image model: Qwen/Qwen-Image-Edit-2511" appearing twice in the startup log. So the image path now does what the text path does: a sibling sharing a checkpoint is registered under its alias, which gives it a config key of its own, and is NOT aliased back to the shared path -- that mapping is what collapses the two again. An unaliased duplicate is skipped with the same explanation text models give. `path` now travels in every image config, because a sibling is addressed by an alias and an alias is not something from_pretrained can open: the config has to be able to say which checkpoint it is a config OF. _load_diffusers_pipeline prefers it when it differs from the name it was called with, and for a primary entry the two are the same string. Needs a restart to take effect, registration being a startup act: a collision that has already happened cannot be undone in the running process's config dict. Full suite 4558 passed with the same 15 pre-existing failures as 0.4.91 (diff empty). Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
0.4.96 added gguf>=0.10.0 to requirements.txt, which was half right. Two things about how this image is built that the change missed: * requirements-oci.txt has carried `gguf>=0.9.0` since June, so the dependency was not undeclared after all -- only unsatisfiable. diffusers' GGUFQuantizationConfig refuses anything below 0.10.0 ("requires both PyTorch and GGUF>=0.10.0"), so a 0.9.x install satisfies the line and still cannot load a GGUF pipeline component. Pinned to 0.10.0. * Dockerfile.oci COPIES requirements.txt, requirements-nvidia.txt and requirements-vulkan.txt into the build and then deletes them unused: the RUN installs `-r /tmp/requirements-oci.txt` and nothing else. So for the container image, requirements-oci.txt is the only list that counts. requirements.txt still matters for a host build.sh install, and the in-image upgrade reads BOTH, which is why the entry there is not wasted. No rebuild needed to ship it: coderai-upgrade's delta install takes requirements.txt plus requirements-oci.txt, installs what is new, and the autoupgrade commits the result onto the image tag. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
Requested for a client that edits photos by instruction -- "make the dress red", "add a necklace" -- which plain img2img cannot do at any strength, because `strength` is the fraction of the source it destroys before re-imagining it. Serving Qwen-Image-Edit / FLUX.1-Kontext class models turned up two latent bugs on the way. The instruction path. /v1/images/edits now shapes its call to what the pipeline accepts, from the signature, rather than branching on the model: QwenImageEditPlus / QwenImageEdit / FluxKontext image ✓ strength ✗ SDXL-Img2Img / QwenImageImg2Img / FluxImg2Img image ✓ strength ✓ Taking an `image` with no `strength` IS the distinction, and it generalises, so no list of class names has to be maintained. `strength` is dropped where it does not exist -- a caller cannot know which kind of model it is addressing, and these pipelines carry no **kwargs to swallow it, so passing it through would be a hard TypeError. `guidance_scale` becomes `true_cfg_scale`, the knob they actually read. Steps default to 4 at cfg 1.0 for them instead of inheriting img2img's 30 at 7.5, which would waste a minute an image and overcook the result. Up to three images may be given (`image` plus `images`) for a composed edit, conditioned at one shape; more is a 400. `instruction_edit` is reported in /v1/models so a client can hide the strength slider and treat the prompt as an instruction. Verified to classify the four Qwen/Kontext/FireRed ids and to leave Pony, Z-Image and Illustrious as img2img. component_models is new: a map from a component to ANOTHER repo, loaded and handed to the pipeline as a prebuilt object. Several Qwen-Image-Edit derivatives ship a transformer and nothing else -- the base supplies the VAE, the text encoder and the scheduler -- and there was previously no way to express that, so they could not be served at all. Its entry in component_quantization still applies, so a transformer too large for the card is quantized as it loads. Two latent bugs, both of which anyone using GGUF image components would have hit: * `gguf` was never a declared dependency. build_gguf_pipeline_components has existed through several releases and could not have worked: diffusers needs gguf>=0.10.0 for GGUFQuantizationConfig and only says so mid-load, as "Unable to load weights from checkpoint file". Now in requirements.txt, which build.sh installs. * from_single_file was called with no `config`, so it INFERRED the architecture -- and its fallback is Stable Diffusion 1.5. A Qwen-Image, Flux or Wan transformer therefore failed with "stable-diffusion-v1-5 does not appear to have a file named config.json". It now passes the base pipeline's own component subfolder, which was already at hand. Measured on an RTX 3090 after the fixes: the Q4_K_M transformer binds as QwenImageTransformer2DModel (13.23B params, 85s) and the assembled QwenImageEditPlusPipeline is 22.68 GiB resident with a 4-bit text encoder -- which fits 24 GB but leaves 1.32 GiB and OOMs on 390 MiB of activations. That is a deployment setting, not a code one, and it is why the figure is measured here rather than asserted. Full suite 4558 passed with the same 15 pre-existing failures as 0.4.91 (diff empty). Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
Fixes a NameError shipped in 0.4.94: `name 'model_key' is not defined` on every /v1/images/edits request. Replacing that endpoint's load block with the shared _pipeline_for_task call removed the line defining model_key, while a line further down still used it to look up the model's config. The inpaint endpoint got its replacement definition; this one did not. Editing was broken from the moment it was deployed, and a client reported it from production. 187 tests covered this code and none of them caught it, because every one read the source as text -- is this guard present, does this call precede that one. A structural test cannot see an undefined name on the first line of the request path. That is a flaw in how the work was tested, not bad luck. So the endpoints are now executed. tests/test_image_edit_endpoints_actually_run.py calls create_image_edit and create_image_inpaint with their collaborators stubbed -- the manager, image decode, the response writer, the pipeline -- so the whole body between the request arriving and the pipeline being called actually runs. Checked the way a regression test has to be: with the bug reintroduced it fails with the production message verbatim, and passes with the fix. One that passed either way would have been worth nothing. The tests assert what the body is for, not just that it returns: that the SOURCE IMAGE arrives at the pipeline at the size requested, that an omitted size keeps the photo's aspect instead of squaring it, that the mask matches the image's shape, that a malformed size is a 400 from the endpoint rather than a 500 from inside it, and that a model's prompt_prefix survives every layer in between. Also noted for whoever reaches for a linter next: a stdlib undefined-name scan was tried first and would NOT have caught this one, since it cannot tell a function-local assignment from a module-level name. Real scope analysis means pyflakes, which is not a dependency here. Full suite 4558 passed with the same 15 pre-existing failures as 0.4.91 (diff empty). Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-
Stefy Lanza (nextime / spora ) authored
A checkpoint is a UNet, a VAE and its text encoders; which task it performs is a property of the pipeline class wrapped around them, not of the weights. Both tasks already worked after 0.4.93, but each endpoint loaded its OWN pipeline under its own key -- so a client that creates a character with a model and then edits it with the same one held an 18 GB SDXL fine-tune twice, and the second load evicted the first. Alternating between the two thrashes the card continuously. Reading the two loaders side by side turned up the other half of it: _load_img2img_pipeline ignored the model's configuration altogether -- dtype hardcoded from global args, no offload, no quantization, no VAE override. Only _load_diffusers_pipeline honours models.json. A 4-bit model was therefore served at 4 bits for generation and bf16 for editing, out of two copies. So the model is loaded once, through the configured loader, under the manager's own key -- its VRAM accounting and eviction now see one model -- and each task gets a view built with AutoPipelineFor*.from_pipe around the components already resident. /v1/images/generations gets the same treatment in reverse, so a model an edit loaded can create a character without reloading. The views are cached on the base pipeline, not as extra models-dict entries. Deliberately: they share the base's tensors and so must die exactly when it does, and a second models-dict key per task is what let a cache key reach from_pretrained in 0.4.93. The img2img:/inpaint: keys are gone from the module entirely. Task detection asks diffusers' own tables rather than a signature, because a ControlNet text-to-image pipeline requires an `image` -- the control hint, not a source -- and a signature check would file it as img2img. Tested. Two behaviours worth knowing: the views share the UNet, so a LoRA or IP-Adapter applied through one task is visible to the others (same weights, by design); and SD, SDXL, Flux and Z-Image all have entries for all three tasks, so this is not an SD-only arrangement. I had assumed Z-Image was unmapped and wrote a test asserting it; the test caught that and both it and a code comment are corrected. 28 tests here, 187 across the image files; full suite 4549 passed with the same 15 pre-existing failures as 0.4.91 (diff empty). Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
-