[Frontend][Model] Add Cosmos3 control uploads to video API (#7027)

Signed-off-by: Fred Huang <frhuang@nvidia.com>
This commit is contained in:
FredHuangNV authored and GitHub committed 2026-09-07 01:38:56 -07:00
1 parent 677265b64b
commit 85b0bad2d3
6 files changed
+434 -9

No files matched your search

+19
View File
@@ -68,6 +68,8 @@ curl -L "http://localhost:8091/v1/videos/${video_id}/content" -o output.mp4
| Parameter | Type | Default | Description |
| ----------- | ------ | --------- | ------------- |
| `input_reference` | file | null | Uploaded reference image or video for image-to-video/video-to-video requests |
| `control_reference` | file | null | Optional uploaded image/video control, up to 512 MiB, for models that declare control-upload support |
| `control_type` | string | null | Model control name associated with `control_reference`; currently Cosmos3 supports `edge`, `blur`, `depth`, `seg`, and `wsm` |
| `image_reference` | string | null | JSON-encoded reference image payload; do not combine with `input_reference` or `video_reference` |
| `video_reference` | string | null | JSON-encoded reference video payload; do not combine with `input_reference` or `image_reference` |
| `audio_reference` | string | null | JSON-encoded audio reference for speech-to-video: `{"audio_url": "..."}` — supports HTTP(s) URLs or base64 data URLs |
@@ -177,6 +179,23 @@ corresponding addition. `negative_metadata_mode` accepts `same`, `inverse`, or
`none` and defaults to `same` for transfer. See the Cosmos3 recipe for complete
examples.
A client that cannot place the control on the server filesystem can upload one
control with `control_reference` and identify it with `control_type`. The API
streams the upload to request-scoped storage, supplies its path to the model,
and removes it after synchronous or asynchronous generation completes. Other
options for the selected control can remain in `extra_params`; do not also set
`control` or `control_path` there. Uploads larger than 512 MiB are rejected.
```bash
curl -s http://localhost:8091/v1/videos/sync \
-F "prompt=Preserve the scene while following the world-state control" \
-F "input_reference=@input.mp4;type=video/mp4" \
-F "control_reference=@wsm.mp4;type=video/mp4" \
-F "control_type=wsm" \
-F 'extra_params={"wsm":{"control_weight":1.0}}' \
-o output.mp4
```
HTTP redirects for `image_reference.image_url` follow vLLM's
`VLLM_MEDIA_URL_ALLOW_REDIRECTS` setting. Before starting the server, set it to
`1` (the default) to allow redirects or `0` to reject them. A redirect target
+12
View File
@@ -207,6 +207,18 @@ curl -sS -X POST http://localhost:8000/v1/videos/sync \
-F 'extra_params={"depth":{"control_path":"/path/to/depth_control.mp4"},"max_frames":121,"resolution":"720","num_video_frames_per_chunk":121}' \
-o cosmos3_transfer_depth.mp4
# The same transfer control can be uploaded by a remote client instead of
# being placed on the server filesystem. Uploads are limited to 512 MiB.
curl -sS -X POST http://localhost:8000/v1/videos/sync \
-H "Accept: video/mp4" \
-F "model=nvidia/Cosmos3-Nano" \
-F "prompt=Generate a realistic scene following the provided world-state control." \
-F "input_reference=@/path/to/input.mp4;type=video/mp4" \
-F "control_reference=@/path/to/wsm.mp4;type=video/mp4" \
-F "control_type=wsm" \
-F 'extra_params={"wsm":{"control_weight":1.0},"max_frames":121,"resolution":"720","num_video_frames_per_chunk":121}' \
-o cosmos3_transfer_wsm.mp4
# Text-to-video-with-sound
curl -sS -X POST http://localhost:8000/v1/videos/sync \
-H "Accept: video/mp4" \
@@ -75,6 +75,7 @@ class FakeAsyncOmni:
self.model_class_name = "WanPipeline"
self.captured_prompt = None
self.captured_reference_video_bytes = None
self.captured_control_reference_bytes = {}
self.captured_sampling_params_list = None
def get_diffusion_od_config(self):
@@ -83,6 +84,10 @@ class FakeAsyncOmni:
async def generate(self, prompt, request_id, sampling_params_list):
self.captured_prompt = prompt
self.captured_sampling_params_list = sampling_params_list
for control_type in ("edge", "blur", "depth", "seg", "wsm"):
control_params = sampling_params_list[0].extra_args.get(control_type)
if isinstance(control_params, dict) and isinstance(control_params.get("control_path"), str):
self.captured_control_reference_bytes[control_type] = Path(control_params["control_path"]).read_bytes()
reference_videos = prompt.get("multi_modal_data", {}).get("video")
if (
isinstance(reference_videos, list)
@@ -338,6 +343,8 @@ def _cosmos3_stage_configs():
return [
SimpleNamespace(
stage_type="diffusion",
final_output=True,
final_output_type="video",
engine_args=SimpleNamespace(model_class_name="Cosmos3OmniDiffusersPipeline"),
)
]
@@ -919,6 +926,17 @@ def test_mixed_reference_capability_uses_model_metadata_when_config_defaults_fal
assert handler.supports_mixed_reference_inputs
@pytest.mark.parametrize("model_class_name", ["Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"])
def test_control_upload_capability_is_declared_only_by_cosmos3(test_client, model_class_name):
handler = test_client.app.state.openai_serving_video
handler._engine_client.model_class_name = model_class_name
assert handler.supported_control_upload_types == frozenset({"edge", "blur", "depth", "seg", "wsm"})
handler._engine_client.model_class_name = "WanPipeline"
assert handler.supported_control_upload_types == frozenset()
def test_decode_video_bytes_can_keep_first_frames():
from vllm_omni.entrypoints.openai.video_api_utils import _decode_video_bytes
@@ -2218,6 +2236,171 @@ def test_sync_v2v_returns_video_bytes(test_client, mocker: MockerFixture):
assert input_video[0].size == (32, 24)
@pytest.mark.parametrize("endpoint", ["/v1/videos", "/v1/videos/sync"])
@pytest.mark.parametrize("control_type", ["wsm", "depth"])
def test_cosmos3_accepts_optional_uploaded_control(
endpoint,
control_type,
test_client,
mocker: MockerFixture,
):
control_bytes = f"{control_type}-control".encode()
_mock_encode_video_bytes(mocker, b"controlled-video")
engine = test_client.app.state.openai_serving_video._engine_client
engine.model_class_name = "Cosmos3OmniDiffusersPipeline"
response = test_client.post(
endpoint,
data={
"prompt": "Follow the uploaded control.",
"control_type": control_type,
"extra_params": json.dumps({control_type: {"control_weight": 0.75}}),
},
files=[
("input_reference", ("input.mp4", _make_test_video_bytes(), "video/mp4")),
("control_reference", (f"{control_type}.mp4", control_bytes, "video/mp4")),
],
)
assert response.status_code == 200
if endpoint.endswith("/sync"):
assert response.content == b"controlled-video"
else:
video_id = response.json()["id"]
_wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
captured = engine.captured_sampling_params_list[0].extra_args[control_type]
assert captured["control_weight"] == 0.75
assert engine.captured_control_reference_bytes[control_type] == control_bytes
assert len(engine.captured_prompt["multi_modal_data"]["video"]) == 3
assert not Path(captured["control_path"]).exists()
@pytest.mark.parametrize("endpoint", ["/v1/videos", "/v1/videos/sync"])
def test_cosmos3_uploaded_control_selects_transfer_reference_video_decode_policy(
endpoint,
test_client,
mocker: MockerFixture,
):
_mock_encode_video_bytes(mocker, b"controlled-video")
test_client.app.state.stage_configs = _cosmos3_stage_configs()
response = test_client.post(
endpoint,
data={
"prompt": "Preserve all conditioning motion.",
"control_type": "wsm",
"num_frames": "9",
"extra_params": json.dumps({"num_first_chunk_conditional_frames": 9}),
},
files=[
(
"input_reference",
("input.mp4", _make_test_video_bytes(num_frames=6), "video/mp4"),
),
("control_reference", ("wsm.mp4", b"control", "video/mp4")),
],
)
assert response.status_code == 200
if not endpoint.endswith("/sync"):
video_id = response.json()["id"]
_wait_for_status(test_client, video_id, VideoGenerationStatus.COMPLETED.value)
engine = test_client.app.state.openai_serving_video._engine_client
assert len(engine.captured_prompt["multi_modal_data"]["video"]) == 6
def test_cosmos3_control_upload_is_optional(test_client, mocker: MockerFixture):
_mock_encode_video_bytes(mocker)
engine = test_client.app.state.openai_serving_video._engine_client
engine.model_class_name = "Cosmos3OmniDiffusersPipeline"
response = test_client.post("/v1/videos/sync", data={"prompt": "No control for this request."})
assert response.status_code == 200
assert "wsm" not in engine.captured_sampling_params_list[0].extra_args
def test_control_upload_does_not_affect_models_without_capability(test_client):
response = test_client.post(
"/v1/videos/sync",
data={"prompt": "Unsupported control.", "control_type": "wsm"},
files={"control_reference": ("wsm.mp4", b"control", "video/mp4")},
)
assert response.status_code == 400
assert "not supported by this model" in response.json()["detail"]
assert test_client.app.state.openai_serving_video._engine_client.captured_prompt is None
@pytest.mark.parametrize(
("data", "files", "message"),
[
(
{"prompt": "Missing type."},
{"control_reference": ("wsm.mp4", b"control", "video/mp4")},
"requires control_type",
),
(
{"prompt": "Missing file.", "control_type": "wsm"},
None,
"requires a control_reference",
),
(
{"prompt": "Unknown type.", "control_type": "unknown"},
{"control_reference": ("control.mp4", b"control", "video/mp4")},
"not supported by this model",
),
],
)
def test_cosmos3_control_upload_validates_contract(data, files, message, test_client):
test_client.app.state.openai_serving_video._engine_client.model_class_name = "Cosmos3OmniDiffusersPipeline"
response = test_client.post("/v1/videos/sync", data=data, files=files)
assert response.status_code == 400
assert message in response.json()["detail"]
def test_cosmos3_control_upload_rejects_existing_control_source(test_client):
test_client.app.state.openai_serving_video._engine_client.model_class_name = "Cosmos3OmniDiffusersPipeline"
response = test_client.post(
"/v1/videos/sync",
data={
"prompt": "Ambiguous control.",
"control_type": "wsm",
"extra_params": json.dumps({"wsm": {"control_path": "/already/present.mp4"}}),
},
files={"control_reference": ("wsm.mp4", b"control", "video/mp4")},
)
assert response.status_code == 400
assert "not both" in response.json()["detail"]
@pytest.mark.parametrize(
("control_bytes", "message"),
[
(b"", "must not be empty"),
(b"control", "size limit"),
],
)
def test_cosmos3_control_upload_rejects_invalid_size(control_bytes, message, test_client, monkeypatch):
test_client.app.state.openai_serving_video._engine_client.model_class_name = "Cosmos3OmniDiffusersPipeline"
monkeypatch.setattr(api_server, "CONTROL_REFERENCE_MAX_BYTES", 3)
response = test_client.post(
"/v1/videos/sync",
data={"prompt": "Invalid control.", "control_type": "wsm"},
files={"control_reference": ("wsm.mp4", control_bytes, "video/mp4")},
)
assert response.status_code == 400
assert message in response.json()["detail"]
assert test_client.app.state.openai_serving_video._engine_client.captured_prompt is None
def test_sync_missing_handler_returns_503():
app = FastAPI()
app.include_router(router)
+12 -2
View File
@@ -11,6 +11,10 @@ class DiffusionModelMetadata:
supports_multimodal_inputs: bool = False
max_multimodal_image_inputs: int | None = None
supports_mixed_reference_inputs: bool = False
# Multipart controls are exposed as ``control_reference`` plus
# ``control_type`` by the video API. A pipeline that opts in receives the
# persisted upload through ``extra_args[control_type]["control_path"]``.
supported_control_upload_types: tuple[str, ...] = ()
attention_mask_free: bool = False
final_output_type: str | None = None
@@ -94,8 +98,14 @@ _DIFFUSION_MODEL_METADATA: dict[str, DiffusionModelMetadata] = {
"LongCatVideoAvatarPipeline": DiffusionModelMetadata(final_output_type="video"),
"MagiHumanPipeline": DiffusionModelMetadata(final_output_type="video"),
"DreamIDOmniPipeline": DiffusionModelMetadata(final_output_type="video"),
"Cosmos3OmniDiffusersPipeline": DiffusionModelMetadata(final_output_type="video"),
"Cosmos3OmniPipeline": DiffusionModelMetadata(final_output_type="video"),
"Cosmos3OmniDiffusersPipeline": DiffusionModelMetadata(
supported_control_upload_types=("edge", "blur", "depth", "seg", "wsm"),
final_output_type="video",
),
"Cosmos3OmniPipeline": DiffusionModelMetadata(
supported_control_upload_types=("edge", "blur", "depth", "seg", "wsm"),
final_output_type="video",
),
"SanaVideoPipeline": DiffusionModelMetadata(final_output_type="video"),
"SanaImageToVideoPipeline": DiffusionModelMetadata(final_output_type="video"),
"SanaWmPipeline": DiffusionModelMetadata(
+179 -7
View File
@@ -15,8 +15,8 @@ import tempfile
import time
import uuid
from argparse import Namespace
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator, Mapping
from contextlib import asynccontextmanager, suppress
from http import HTTPStatus
from numbers import Integral
from pathlib import Path
@@ -181,6 +181,9 @@ MINIMAX_H3_MAX_REFERENCE_COUNT = 12
MINIMAX_H3_REFERENCE_IMAGE_FORMATS = frozenset({"jpeg", "png", "webp", "heic", "heif"})
MINIMAX_H3_REFERENCE_VIDEO_SUFFIXES = frozenset({".mp4", ".mov"})
MINIMAX_H3_REFERENCE_AUDIO_SUFFIXES = frozenset({".wav", ".mp3"})
CONTROL_REFERENCE_IMAGE_SUFFIXES = frozenset({".bmp", ".gif", ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp"})
CONTROL_REFERENCE_VIDEO_SUFFIXES = frozenset({".mkv", ".mov", ".mp4", ".webm"})
CONTROL_REFERENCE_MAX_BYTES = 512 * 1024 * 1024
profiler_router = APIRouter()
@@ -3073,6 +3076,7 @@ async def _cleanup_video(video_id: str):
def _cleanup_video_references(
reference_video: ReferenceVideo | None,
reference_audio: ReferenceAudio | None,
control_path: str | None = None,
) -> None:
if reference_video is not None:
for path in reference_video.cleanup_paths:
@@ -3083,6 +3087,8 @@ def _cleanup_video_references(
for path in cleanup_paths:
if os.path.exists(path):
os.unlink(path)
if control_path is not None and os.path.exists(control_path):
os.unlink(control_path)
async def _run_video_generation_job(
@@ -3092,11 +3098,13 @@ async def _run_video_generation_job(
reference_image: ReferenceImage | None = None,
reference_video: ReferenceVideo | None = None,
reference_audio: ReferenceAudio | None = None,
control_path: str | None = None,
app_state: Any | None = None,
) -> None:
job = await VIDEO_STORE.get(video_id)
if job is None:
logger.warning("Video job %s missing before generation task started; skipping", video_id)
_cleanup_video_references(reference_video, reference_audio, control_path)
return
await VIDEO_STORE.update_fields(video_id, {"status": VideoGenerationStatus.IN_PROGRESS})
@@ -3165,7 +3173,7 @@ async def _run_video_generation_job(
await VIDEO_STORE.pop(video_id)
raise
finally:
_cleanup_video_references(reference_video, reference_audio)
_cleanup_video_references(reference_video, reference_audio, control_path)
VIDEO_SYNC_TIMEOUT_S = float(os.environ.get("VLLM_OMNI_VIDEO_SYNC_TIMEOUT", 600.0))
@@ -3191,6 +3199,121 @@ async def _persist_uploaded_video_references(uploads: list[UploadFile]) -> list[
return paths
async def _persist_uploaded_control_reference(
upload: UploadFile,
*,
max_bytes: int = CONTROL_REFERENCE_MAX_BYTES,
) -> str:
"""Stream one model control upload to request-scoped local storage."""
kind = _uploaded_media_kind(upload)
suffix = Path(upload.filename or "").suffix.lower()
supported_suffixes = CONTROL_REFERENCE_IMAGE_SUFFIXES | CONTROL_REFERENCE_VIDEO_SUFFIXES
if kind == "audio" or (suffix and suffix not in supported_suffixes):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail="control_reference must be an image or video file.",
)
if not suffix:
suffix = ".png" if kind == "image" else ".mp4"
declared_size = getattr(upload, "size", None)
if isinstance(declared_size, Integral) and int(declared_size) > max_bytes:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail=f"control_reference exceeds the {max_bytes // (1024 * 1024)} MiB size limit.",
)
fd, path = tempfile.mkstemp(prefix="vllm_omni_control_reference_", suffix=suffix)
size = 0
persisted = False
try:
with os.fdopen(fd, "wb") as output:
while chunk := await upload.read(min(1024 * 1024, max_bytes - size + 1)):
size += len(chunk)
if size > max_bytes:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail=f"control_reference exceeds the {max_bytes // (1024 * 1024)} MiB size limit.",
)
output.write(chunk)
if size == 0:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail="control_reference must not be empty.",
)
persisted = True
return path
finally:
if not persisted:
with suppress(OSError):
os.unlink(path)
def _validate_control_upload(
handler: OmniOpenAIServingVideo,
request: VideoGenerationRequest,
control_reference: UploadFile | None,
control_type: str | None,
) -> str | None:
"""Validate a generic uploaded control against model capabilities."""
if control_reference is None:
if control_type is not None:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail="control_type requires a control_reference upload.",
)
return None
if control_type is None or not control_type.strip():
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail="control_reference requires control_type.",
)
normalized_type = control_type.strip().lower()
supported_types = frozenset(getattr(handler, "supported_control_upload_types", ()))
if normalized_type not in supported_types:
supported = ", ".join(sorted(supported_types)) or "none"
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail=(
f"Control upload type '{normalized_type}' is not supported by this model. "
f"Supported control upload types: {supported}."
),
)
existing = (request.extra_params or {}).get(normalized_type)
if existing is not None:
if not isinstance(existing, Mapping) and existing is not True:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail=f"extra_params.{normalized_type} must be an object when control_reference is uploaded.",
)
if isinstance(existing, Mapping) and any(existing.get(key) is not None for key in ("control", "control_path")):
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST.value,
detail=(
"Provide either control_reference or "
f"extra_params.{normalized_type}.control/control_path, not both."
),
)
return normalized_type
def _attach_control_upload(
request: VideoGenerationRequest,
control_type: str,
control_path: str | None = None,
) -> None:
"""Declare an uploaded control and attach its persisted path when available."""
extra_params = dict(request.extra_params or {})
existing = extra_params.get(control_type)
control_params = dict(existing) if isinstance(existing, Mapping) else {}
if control_path is not None:
control_params["control_path"] = control_path
extra_params[control_type] = control_params
request.extra_params = extra_params
def _reference_list(value: Any) -> list[Any]:
if value is None:
return []
@@ -3337,6 +3460,8 @@ async def _parse_video_form(
prompt: str = Form(...),
input_reference: UploadFile | None = File(default=None),
input_references: list[UploadFile] | None = File(default=None),
control_reference: UploadFile | None = File(default=None),
control_type: str | None = Form(default=None),
image_reference: str | None = Form(default=None),
video_reference: str | None = Form(default=None),
audio_reference: str | None = Form(default=None),
@@ -3376,6 +3501,7 @@ async def _parse_video_form(
ReferenceImage | None,
ReferenceVideo | None,
ReferenceAudio | None,
str | None,
]:
"""FastAPI dependency that parses video form data, validates inputs,
resolves the handler, and decodes any reference image.
@@ -3470,6 +3596,13 @@ async def _parse_video_form(
detail=f"Video generation setup failed: {str(e)}",
)
normalized_control_type = _validate_control_upload(handler, request, control_reference, control_type)
if normalized_control_type is not None:
# Make the selected transfer mode visible while choosing the model's
# reference-video decode policy. The upload itself stays unpersisted
# until reference parsing succeeds, preserving failure cleanup.
_attach_control_upload(request, normalized_control_type)
supports_mixed_reference_inputs = bool(getattr(handler, "supports_mixed_reference_inputs", False))
if input_reference is not None:
input_reference_bytes = await _read_upload_limited(
@@ -3592,7 +3725,27 @@ async def _parse_video_form(
cleanup_paths=cleanup_paths,
)
return request, handler, effective_model_name, reference_image, reference_video, reference_audio
control_path: str | None = None
if control_reference is not None and normalized_control_type is not None:
try:
control_path = await _persist_uploaded_control_reference(
control_reference,
max_bytes=CONTROL_REFERENCE_MAX_BYTES,
)
_attach_control_upload(request, normalized_control_type, control_path)
except (asyncio.CancelledError, HTTPException, OSError, TypeError, ValueError):
_cleanup_video_references(reference_video, reference_audio, control_path)
raise
return (
request,
handler,
effective_model_name,
reference_image,
reference_video,
reference_audio,
control_path,
)
@router.post(
@@ -3613,6 +3766,7 @@ async def create_video(
ReferenceImage | None,
ReferenceVideo | None,
ReferenceAudio | None,
str | None,
] = Depends(_parse_video_form),
) -> VideoResponse:
"""Create an asynchronous video generation job.
@@ -3620,7 +3774,15 @@ async def create_video(
Accepts multipart form-data (see ``_parse_video_form`` for parameters),
persists a queued job record, and starts generation in the background.
"""
request, handler, effective_model_name, reference_image, reference_video, reference_audio = ctx
(
request,
handler,
effective_model_name,
reference_image,
reference_video,
reference_audio,
control_path,
) = ctx
ref = video_response_from_request(effective_model_name, request)
await VIDEO_STORE.upsert(ref.id, ref)
task = asyncio.create_task(
@@ -3631,6 +3793,7 @@ async def create_video(
reference_image,
reference_video,
reference_audio,
control_path,
app_state=raw_request.app.state,
)
)
@@ -3656,6 +3819,7 @@ async def create_video_sync(
ReferenceImage | None,
ReferenceVideo | None,
ReferenceAudio | None,
str | None,
] = Depends(_parse_video_form),
) -> Response:
"""Synchronous video generation endpoint.
@@ -3667,7 +3831,15 @@ async def create_video_sync(
Metadata is returned via response headers ``X-Request-Id``,
``X-Model``, and ``X-Inference-Time-S``.
"""
request, handler, effective_model_name, reference_image, reference_video, reference_audio = ctx
(
request,
handler,
effective_model_name,
reference_image,
reference_video,
reference_audio,
control_path,
) = ctx
request_id = f"video_sync-{random_uuid()}"
raw_request.state.request_metadata = RequestResponseMetadata(request_id=request_id)
started_at = time.perf_counter()
@@ -3701,7 +3873,7 @@ async def create_video_sync(
detail=f"Video generation failed: {str(exc)}",
) from exc
finally:
_cleanup_video_references(reference_video, reference_audio)
_cleanup_video_references(reference_video, reference_audio, control_path)
inference_time_s = time.perf_counter() - started_at
return Response(
@@ -182,6 +182,35 @@ class OmniOpenAIServingVideo:
)
return capability is True or metadata_capability
@property
def supported_control_upload_types(self) -> frozenset[str]:
"""Return multipart control types accepted by the active pipeline.
Unknown pipelines deliberately return an empty set. This keeps the
generic video API isolated from model-specific controls unless a model
explicitly opts into the ``control_path`` contract in metadata.
"""
od_config = self._resolve_diffusion_od_config()
model_archs = [None if od_config is None else getattr(od_config, "model_class_name", None)]
for stage_config in self.stage_configs or ():
stage_get = (
stage_config.get if isinstance(stage_config, Mapping) else lambda key: getattr(stage_config, key, None)
)
engine_args = stage_get("engine_args") or {}
model_archs.extend(
(
stage_get("model_arch"),
engine_args.get("model_class_name")
if isinstance(engine_args, Mapping)
else getattr(engine_args, "model_class_name", None),
)
)
supported: set[str] = set()
for model_arch in model_archs:
supported.update(get_diffusion_model_metadata(model_arch).supported_control_upload_types)
return frozenset(supported)
@classmethod
def for_diffusion(
cls,