serve the LTX-2 family, fetch a Civitai checkpoint, and stop walking 900 GB of...

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: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8LAP3ueNzQJtwzhgVbq66
parent 3f9b2680
# A model's own settings, and where its weights come from
Two things an operator configures per model that clients then stop guessing at:
the settings the model was published with, and the file the weights live in.
## `defaults` — the card's own recommendation
```jsonc
{
"alias": "Krea-2-Turbo",
"path": "krea/Krea-2-Turbo",
"defaults": {
"steps": 8,
"guidance_scale": 1.0,
"sampler": "flow_euler",
"modes": ["t2i"],
"license": "Krea 2 licence — requires a content filter"
}
}
```
These **fill in what a request omits and never override what it sends.** That
is the whole distinction from `call_kwargs`, which applies on every call
because the card says the model does not work without it.
Why it exists: a distilled model wants 8 steps at CFG 1 and its non-distilled
sibling wants 30 at CFG 4. Without this every client keeps its own table of
per-model settings, keyed on the model **name** — which breaks the moment a
model is renamed or a second quantization of it is added — and a client with no
table runs an 8-step model for 30 steps: four times the cost for a worse
picture.
Precedence, highest first:
1. what the request asked for;
2. a **fused** distillation preset detected on the pipeline (`acceleration`),
because it knows the LoRA actually baked in;
3. `defaults` from the model entry;
4. the generic floor (30 steps / CFG 7.5 for images, 25 / 7.5 for video).
`/v1/models` publishes `defaults`, `modes` and `license` so a UI can show them
instead of inventing them. `modes` is read from `defaults.modes`, a top-level
`modes`, or a video entry's long-standing `video_subtypes` — whichever the
operator used.
### `sampler`
A request may carry `sampler`, and a model entry may default it:
```
euler euler_a heun dpm / dpmpp_2m dpmpp_sde dpmpp_2s ddim ddpm
pndm lms deis unipc lcm kdpm2 kdpm2_a
flow_euler flow_heun flow_lcm
```
The `flow_*` family is for flow-matching models (FLUX, SD3, Z-Image, LTX) and
**cannot be mixed with the rest.** Handing a FLUX-family pipeline a DDIM does
not give a worse picture, it gives noise, so crossing that boundary is refused:
a 400 when the request asked, a logged warning when a model card asked (the
request itself is fine and should not be failed over the operator's config).
An unknown name is a 400 listing the valid ones.
## Where the weights come from
Three shapes, all accepted wherever a model `path` is given:
| Reference | Means |
|---|---|
| `org/repo` | a HuggingFace repo in the normal diffusers/transformers layout |
| `/AI/models/someMix_v18.safetensors` | one checkpoint file on this machine |
| `org/repo:someMix_v18.safetensors` | one file out of a repo holding several versions |
| `civitai:1609320` | one checkpoint from Civitai, by **model version id** |
### `civitai:` references
A large part of the community's fine-tunes are published only on Civitai, as a
single ComfyUI-style checkpoint — exactly the shape `from_single_file` already
handles. The only thing missing was a way to fetch one, so an operator had to
download by hand and hard-code a path.
It is the **version** id, not the model id. A Civitai "model" is a family with
many versions, and an entry naming the family would silently change underneath
you when a new version was published.
Downloaded once into `<cache>/civitai/<id>/` and never fetched again. Most
models need a token:
```bash
export CIVITAI_TOKEN=… # or write it to <config>/civitai_token
```
Without one the API answers with a **web page**, HTTP 200. A naive fetch saves
that to a `.safetensors` name and the problem surfaces hours later as an
unreadable tensor header, so this checks the content type and the size and
refuses, naming the missing token.
## A GGUF component that cannot be loaded is an error
`component_quantization` can point a pipeline component at a GGUF file:
```jsonc
"component_quantization": {
"transformer": "/AI/guffcache/wan22Remix_i2vHighV30-Q4_K_M.gguf",
"text_encoder": "4bit",
"vae": "4bit"
}
```
If a component named here **fails** to load, the load now fails too. It used to
be skipped with a log line — and a skipped component is simply absent, 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
actually needed. A silent downgrade to something that does not fit is worse
than a refusal.
## Pre-quantized checkpoints need no `load_in_4bit`
A repo published already quantized — `…-bnb-4bit`, an AWQ or GPTQ build, an fp8
release — carries a `quantization_config` in its `config.json`, and
transformers/diffusers load those weights as they are. Leave `load_in_4bit`
unset: there is nothing to ask for.
The VRAM estimator detects this and stops normalising the on-disk size up to
the load precision. It did not, once, and a 16.3 GB 4-bit checkpoint was
estimated at **30.03 GB against a 24 GB card** — so nothing could ever free
enough, the pipeline ran partly on the CPU, and a few-step turbo model took 72
seconds an image.
Related: quantized weights cannot be CPU-offloaded at all. A quantized kernel
cannot read a CPU tensor, so such a model loads and then fails **every**
request with `Pointer argument (at 0) cannot be accessed from Triton`. That is
refused at load time, and a quantized model attempts full-GPU regardless of
what the estimate says.
"""C4: fetching a Civitai checkpoint.
A large part of the community's fine-tunes are published only on Civitai, as
one ComfyUI-style checkpoint file -- exactly the shape
build_single_file_pipeline already handles. The only thing missing was a way
to fetch them, so an operator had to download by hand and hard-code a path.
"""
import os
import pytest
from codai.models.hf_loading import (civitai_token, is_single_file_ref,
resolve_civitai, resolve_single_file)
@pytest.mark.parametrize("ref,expect", [
("civitai:1609320", True),
("CIVITAI:153568", True),
("/AI/models/someMix_v18.safetensors", True),
("org/repo:someMix_v18.safetensors", True),
("org/repo", False),
("", False),
(None, False),
])
def test_a_civitai_reference_is_recognised_as_a_single_file(ref, expect):
# It names a version id, not a filename, so there is no suffix to test --
# but it is unambiguously one checkpoint.
assert is_single_file_ref(ref) is expect
def test_the_version_id_is_required_and_must_be_a_number():
# A Civitai "model" is a family with many versions. A config naming the
# family would silently change underneath the operator when a new version
# was published, so the version is what gets named.
for bad in ("civitai:", "civitai:abc", "civitai:1.2", "civitai:v3"):
with pytest.raises(ValueError) as e:
resolve_civitai(bad)
assert "modelVersionId" in str(e.value)
def test_a_cached_checkpoint_is_not_downloaded_again(monkeypatch, tmp_path):
import codai.models.hf_loading as H
d = tmp_path / "civitai" / "999"
d.mkdir(parents=True)
f = d / "RealDream.safetensors"
f.write_bytes(b"x" * 2_000_000)
monkeypatch.setattr(H, "legacy_style_cache_root", lambda: tmp_path,
raising=False)
import codai.platform_paths as PP
monkeypatch.setattr(PP, "legacy_style_cache_root", lambda: tmp_path)
# Nothing may reach the network: a cached file is returned before any
# request is made, so a urlopen call here is a bug.
import urllib.request
monkeypatch.setattr(urllib.request, "urlopen",
lambda *a, **k: pytest.fail("re-downloaded a cached file"))
assert resolve_civitai("civitai:999") == str(f)
def test_the_largest_cached_file_wins(monkeypatch, tmp_path):
import codai.platform_paths as PP
monkeypatch.setattr(PP, "legacy_style_cache_root", lambda: tmp_path)
d = tmp_path / "civitai" / "888"
d.mkdir(parents=True)
(d / "small.safetensors").write_bytes(b"x" * 1_500_000)
big = d / "big.safetensors"
big.write_bytes(b"x" * 3_000_000)
assert resolve_civitai("civitai:888") == str(big)
def test_an_html_error_page_is_not_saved_as_a_checkpoint(monkeypatch, tmp_path):
"""The failure this guards against.
Civitai answers an unauthenticated download with a web page, HTTP 200. A
naive fetch writes it to a .safetensors name and the problem surfaces
hours later as an unreadable tensor header.
"""
import io
import urllib.request
import codai.platform_paths as PP
monkeypatch.setattr(PP, "legacy_style_cache_root", lambda: tmp_path)
class _Resp(io.BytesIO):
headers = {"Content-Type": "text/html; charset=utf-8"}
def __enter__(self): return self
def __exit__(self, *a): return False
monkeypatch.setattr(urllib.request, "urlopen",
lambda *a, **k: _Resp(b"<html>login</html>"))
with pytest.raises(RuntimeError) as e:
resolve_civitai("civitai:777")
msg = str(e.value)
assert "web page" in msg
assert "authentication" in msg
# And nothing was left behind pretending to be a checkpoint.
d = tmp_path / "civitai" / "777"
assert not any(p.suffix == ".safetensors" for p in d.iterdir())
def test_a_truncated_download_is_rejected(monkeypatch, tmp_path):
import io
import urllib.request
import codai.platform_paths as PP
monkeypatch.setattr(PP, "legacy_style_cache_root", lambda: tmp_path)
class _Resp(io.BytesIO):
headers = {"Content-Type": "application/octet-stream"}
def __enter__(self): return self
def __exit__(self, *a): return False
monkeypatch.setattr(urllib.request, "urlopen",
lambda *a, **k: _Resp(b"tiny"))
with pytest.raises(RuntimeError) as e:
resolve_civitai("civitai:666")
assert "not a checkpoint" in str(e.value)
def test_the_missing_token_is_named_in_the_error(monkeypatch, tmp_path):
import io
import urllib.error
import urllib.request
import codai.models.hf_loading as H
import codai.platform_paths as PP
monkeypatch.setattr(PP, "legacy_style_cache_root", lambda: tmp_path)
monkeypatch.setattr(H, "civitai_token", lambda: None)
def _boom(*a, **k):
raise urllib.error.HTTPError("u", 401, "Unauthorized", {}, None)
monkeypatch.setattr(urllib.request, "urlopen", _boom)
with pytest.raises(RuntimeError) as e:
resolve_civitai("civitai:555")
msg = str(e.value)
assert "401" in msg
assert "CIVITAI_TOKEN" in msg
def test_the_token_comes_from_the_environment_or_the_config(monkeypatch, tmp_path):
import codai.models.hf_loading as H
import codai.platform_paths as PP
monkeypatch.setenv("CIVITAI_TOKEN", "from-env")
assert H.civitai_token() == "from-env"
monkeypatch.delenv("CIVITAI_TOKEN", raising=False)
monkeypatch.setattr(PP, "legacy_style_config_dir", lambda: tmp_path)
assert H.civitai_token() is None
(tmp_path / "civitai_token").write_text("from-file\n")
assert H.civitai_token() == "from-file"
def test_resolve_single_file_routes_civitai_without_touching_hf(monkeypatch, tmp_path):
import codai.models.hf_loading as H
called = {}
def _fake(ref):
called["ref"] = ref
return "/tmp/x.safetensors"
monkeypatch.setattr(H, "resolve_civitai", _fake)
assert H.resolve_single_file("civitai:4242") == "/tmp/x.safetensors"
assert called["ref"] == "civitai:4242"
"""The HF cache scan was walking 900 GB of blobs on the event loop.
``huggingface_hub.scan_cache_dir`` stats every blob of every repo. Measured on
this machine: **27 seconds**. It was called from a per-model VRAM estimate, a
GGUF path lookup and an engine-state poll -- the last of which is polled -- so
the loop watchdog recorded 6631 stalls over 0.5s with a worst of 13.0s, during
every one of which no request of any kind was served.
"""
import time
import pytest
def test_the_scan_is_memoised():
import codai.models.cache as C
calls = []
class _Info:
repos = ()
def _fake(_dir):
calls.append(_dir)
return _Info()
C.invalidate_hf_cache_scan()
import huggingface_hub
orig = huggingface_hub.scan_cache_dir
huggingface_hub.scan_cache_dir = _fake
try:
a = C._scan_hf_cache("/tmp/hfcache-x")
b = C._scan_hf_cache("/tmp/hfcache-x")
c = C._scan_hf_cache("/tmp/hfcache-x")
finally:
huggingface_hub.scan_cache_dir = orig
assert a is b is c
assert len(calls) == 1, calls
def test_different_cache_dirs_are_memoised_separately():
import codai.models.cache as C
calls = []
class _Info:
def __init__(self, d):
self.repos = ()
self.d = d
import huggingface_hub
orig = huggingface_hub.scan_cache_dir
huggingface_hub.scan_cache_dir = lambda d: (calls.append(d), _Info(d))[1]
try:
C.invalidate_hf_cache_scan()
a = C._scan_hf_cache("/tmp/one")
b = C._scan_hf_cache("/tmp/two")
finally:
huggingface_hub.scan_cache_dir = orig
assert a is not b
assert calls == ["/tmp/one", "/tmp/two"]
def test_invalidation_forces_a_fresh_scan():
# A model downloaded while the server runs has to become visible.
import codai.models.cache as C
calls = []
import huggingface_hub
orig = huggingface_hub.scan_cache_dir
huggingface_hub.scan_cache_dir = lambda d: (calls.append(d), type(
"I", (), {"repos": ()})())[1]
try:
C.invalidate_hf_cache_scan()
C._scan_hf_cache("/tmp/inv")
C._scan_hf_cache("/tmp/inv")
assert len(calls) == 1
C.invalidate_hf_cache_scan()
C._scan_hf_cache("/tmp/inv")
assert len(calls) == 2
finally:
huggingface_hub.scan_cache_dir = orig
def test_the_ttl_is_short_enough_to_notice_a_download():
# Permanent caching would hide a model that arrived after boot; the window
# has to be far below how long any download takes and far above the poll
# interval.
import codai.models.cache as C
assert 5.0 <= C._HF_SCAN_TTL <= 120.0
def test_a_failed_scan_answers_none_rather_than_raising():
import codai.models.cache as C
import huggingface_hub
orig = huggingface_hub.scan_cache_dir
def _boom(_d):
raise OSError("permission denied")
huggingface_hub.scan_cache_dir = _boom
try:
C.invalidate_hf_cache_scan()
assert C._scan_hf_cache("/tmp/boom") is None
finally:
huggingface_hub.scan_cache_dir = orig
def test_every_estimator_site_uses_the_memoised_scan():
"""The four call sites in the VRAM estimator, which is the hot path.
Asserted on the source because the alternative is a 27-second test.
"""
import inspect
from codai.models import manager
src = inspect.getsource(manager)
memoised = src.count(
"from codai.models.cache import _scan_hf_cache as scan_cache_dir")
direct = src.count("from huggingface_hub import scan_cache_dir")
assert memoised == 4, memoised
assert direct == 0, "an estimator site still walks the cache directly"
# Each site must also treat the None answer as "not found", the way it
# treated an exception before.
assert src.count("if info is None:") == 4
def test_the_engine_state_poll_does_not_block_the_loop():
import inspect
from codai.api import app
src = inspect.getsource(app.internal_engine_state)
assert "asyncio.to_thread(_footprint" in src, (
"the per-model VRAM estimate is back on the event loop")
"""C2/C3/C1: the LTX-2 pipeline split, the new lip-sync engines, Stable Audio.
Each of these was a case where the name matched but the code did not: an LTX-2
checkpoint handed to the LTX 0.9 pipeline, a Stable Audio entry handed to
AudioLDM2, a GGUF component that failed to load and was quietly replaced by
full-precision weights.
"""
import pytest
# ------------------------------------------------------ LTX 0.9 vs LTX 2.x
@pytest.mark.parametrize("name,mode,expect", [
("Lightricks/LTX-2.5", "t2v", "LTX2Pipeline"),
("Lightricks/LTX-2.5", "i2v", "LTX2ImageToVideoPipeline"),
("Lightricks/LTX-2.5", "ti2v", "LTX2ImageToVideoPipeline"),
("LTX2.3-10Eros", "t2v", "LTX2Pipeline"),
("ltx_2-something", "t2v", "LTX2Pipeline"),
("PinkCherry_FineTune_Q5_K_M_v18_LTX23", "i2v", "LTX2ImageToVideoPipeline"),
# The 0.9 family must keep its own pipeline: they are different
# architectures that happen to share a name.
("Lightricks/LTX-Video", "t2v", "LTXPipeline"),
("Lightricks/LTX-Video-0.9.7", "t2v", "LTXPipeline"),
("Lightricks/LTX-Video-0.9.7", "i2v", "LTXImageToVideoPipeline"),
])
def test_the_ltx_version_picks_the_pipeline(name, mode, expect):
pytest.importorskip("diffusers")
from codai.api.video import _detect_pipeline_class
assert _detect_pipeline_class(name, mode).__name__ == expect
def test_ltx2_support_really_is_present_in_this_diffusers():
# The claim above is only meaningful if the classes exist; an LTX-2 entry
# was parked for exactly this reason.
diffusers = pytest.importorskip("diffusers")
for n in ("LTX2Pipeline", "LTX2ImageToVideoPipeline",
"LTX2VideoTransformer3DModel", "AutoencoderKLLTX2Video",
# The audio VAE is why LTX-2 can return a soundtrack with the
# picture, which no other video model here does.
"AutoencoderKLLTX2Audio"):
assert hasattr(diffusers, n), n
# ------------------------------------------------------------ audio backends
@pytest.mark.parametrize("name,expect", [
("stabilityai/stable-audio-open-1.0", "stable_audio"),
("stable_audio_open", "stable_audio"),
("cvssp/audioldm2", "audioldm"),
("facebook/musicgen-large", "musicgen"),
("facebook/audiogen-medium", "audiogen"),
("ACE-Step/ACE-Step-v1-3.5B", "ace_step"),
("acestep-3.5b", "ace_step"),
])
def test_each_audio_model_gets_its_own_family(name, expect):
from codai.api.audio_gen import _detect_audio_gen_type
assert _detect_audio_gen_type(name) == expect
def test_stable_audio_is_not_routed_to_audioldm():
# It was. AudioLDM2Pipeline is a different architecture with a different
# projection model and sample rate, so a configured stable-audio entry
# could only ever fail at load.
from codai.api.audio_gen import _detect_audio_gen_type
assert _detect_audio_gen_type("stabilityai/stable-audio-open-1.0") != "audioldm"
def test_stable_audio_has_a_loader_and_ace_step_says_why_it_has_none():
import inspect
from codai.api import audio_gen
assert hasattr(audio_gen, "_load_stable_audio")
src = inspect.getsource(audio_gen)
i = src.index("elif model_type == 'ace_step':")
window = src[i:i + 700]
# Named plainly rather than loaded as a MusicGen and failing with a shape
# error minutes later.
assert "acestep" in window and "own venv" in window
def test_a_stereo_waveform_is_written_as_stereo():
"""Stable Audio's point is a usable stereo bed; folding it to mono throws
that away, and writing (channels, samples) as-is gives a 2-sample file."""
import io
import numpy as np
import scipy.io.wavfile as wavfile
# (channels, samples) as the pipeline returns it, transposed the way the
# writer now does, then round-tripped.
audio = np.zeros((2, 4800), dtype="float32")
audio[0, :] = 0.5
arr = audio
if arr.ndim > 1 and arr.shape[0] < arr.shape[-1]:
arr = arr.T
buf = io.BytesIO()
wavfile.write(buf, 44100, (np.clip(arr, -1, 1) * 32767).astype(np.int16))
buf.seek(0)
sr, back = wavfile.read(buf)
assert sr == 44100
assert back.shape == (4800, 2), back.shape
def test_a_hot_sample_is_clipped_not_wrapped():
# A float above 1.0 cast straight to int16 wraps to a loud NEGATIVE
# sample, which is heard as a click.
import numpy as np
hot = np.array([1.4, -1.4], dtype="float32")
clipped = (np.clip(hot, -1.0, 1.0) * 32767).astype(np.int16)
assert clipped[0] > 0 and clipped[1] < 0
naive = (hot * 32767).astype(np.int16)
assert naive[0] < 0, "the wrap this guards against no longer happens"
# ------------------------------------------------------- lip-sync engines
def test_the_new_engines_are_video_driven_and_preferred():
from codai.compose import presenter as P
assert "latentsync" in P.VIDEO_ENGINES
assert "musetalk" in P.VIDEO_ENGINES
# Both beat wav2lip for a clip: wav2lip works at 96x96 and upscales, which
# is the soft mouth on a 1080p reel.
order = list(P._AUTO_WITH_BASE)
assert order.index("latentsync") < order.index("wav2lip")
assert order.index("musetalk") < order.index("wav2lip")
# For a still, sadtalker still leads: it invents head motion rather than
# replacing a mouth, which the others cannot do.
assert P._AUTO_STILL[0] == "sadtalker"
def test_an_engine_is_only_available_when_its_assets_are_here(monkeypatch, tmp_path):
from codai.compose import presenter as P
monkeypatch.setattr(P.shutil, "which", lambda n: None)
monkeypatch.setattr(P, "_managed_engine_dir", lambda n: tmp_path / n)
# Nothing installed.
assert P._managed_engine_ready("latentsync") is False
# An entrypoint but no weights is NOT ready: a compose job is the wrong
# place to discover a 5 GB download is missing.
d = tmp_path / "latentsync"
(d / "scripts").mkdir(parents=True)
(d / "scripts" / "inference.py").write_text("x")
assert P._managed_engine_ready("latentsync") is False
(d / "unet.pt").write_text("w")
assert P._managed_engine_ready("latentsync") is True
def test_available_engines_is_reported_in_preference_order(monkeypatch):
from codai.compose import presenter as P
monkeypatch.setattr(P.shutil, "which",
lambda n: "/usr/bin/" + n if n in
("sadtalker", "musetalk", "latentsync") else None)
monkeypatch.setattr(P, "_managed_engine_ready", lambda n: False)
import codai.api.lipsync as L
monkeypatch.setattr(L, "is_available", lambda: True)
got = P.available_engines()
# Discovery order would put sadtalker first; the listing must agree with
# what engine:"auto" would actually choose.
assert got.index("latentsync") < got.index("musetalk") < got.index("wav2lip")
def test_picking_an_uninstalled_engine_names_what_is_installed(monkeypatch):
from codai.compose import presenter as P
monkeypatch.setattr(P, "available_engines", lambda: ["wav2lip"])
monkeypatch.setattr(P.shutil, "which", lambda n: None)
with pytest.raises(P.PresenterError) as e:
P.pick_engine("latentsync", True)
assert "latentsync" in str(e.value) and "wav2lip" in str(e.value)
def test_run_can_set_cwd_and_pythonpath():
# A managed checkout's inference.py imports its own package as a
# top-level module, so it only resolves from inside the checkout.
import inspect
from codai.compose import presenter as P
sig = inspect.signature(P._run)
assert "cwd" in sig.parameters and "pythonpath" in sig.parameters
# ------------------------------------------- a GGUF component that fails
def test_a_failed_gguf_component_refuses_instead_of_downgrading():
"""The silent substitution this prevents.
A component named in component_quantization that does not load is simply
absent from the returned dict, so the base pipeline loads its own
full-precision copy: the operator asked for 14 GB and got 38, while the
VRAM estimate still said 14 and the planner freed a third of what the load
needed.
"""
import inspect
from codai.models import hf_loading
src = inspect.getsource(hf_loading)
i = src.index("gguf_entries = {n: v for n, v in comp_q.items()")
window = src[i:i + 4000]
assert "problems" in window
assert "raise RuntimeError" in window
assert "silently substitute" in window
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment