Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions inference/core/workflows/core_steps/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,9 @@
from inference.core.workflows.core_steps.models.foundation.cosmos3.v1 import (
Cosmos3EdgeBlockV1,
)
from inference.core.workflows.core_steps.models.foundation.cosmos_anomalygen.v1 import (
CosmosAnomalyGenBlockV1,
)

if not ENABLE_TENSOR_DATA_REPRESENTATION:
from inference.core.workflows.core_steps.models.foundation.depth_estimation.v1 import (
Expand Down Expand Up @@ -1932,6 +1935,7 @@ def load_blocks() -> List[Type[WorkflowBlock]]:
GoogleGemmaBlockV3,
ImageSlicerBlockV2,
Cosmos3EdgeBlockV1,
CosmosAnomalyGenBlockV1,
Qwen25VLBlockV1,
Qwen3VLBlockV1,
Qwen35VLBlockV1,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
from typing import List, Literal, Optional, Type, Union

import cv2
import numpy as np
import supervision as sv
from pydantic import ConfigDict, Field
from supervision.draw.color import Color

from inference.core.env import (
ALLOW_INFERENCE_MODELS_DIRECTLY_ACCESS_LOCAL_PACKAGES,
ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES,
)
from inference.core.roboflow_api import get_extra_weights_provider_headers
from inference.core.workflows.core_steps.common.entities import StepExecutionMode
from inference.core.workflows.execution_engine.entities.base import (
OutputDefinition,
WorkflowImageData,
)
from inference.core.workflows.execution_engine.entities.types import (
BOOLEAN_KIND,
FLOAT_KIND,
IMAGE_KIND,
INSTANCE_SEGMENTATION_PREDICTION_KIND,
INTEGER_KIND,
ROBOFLOW_MODEL_ID_KIND,
STRING_KIND,
Selector,
)
from inference.core.workflows.prototypes.block import (
BlockResult,
DependentResource,
Runtime,
RuntimeRestriction,
Severity,
WorkflowBlock,
WorkflowBlockManifest,
roboflow_platform_model,
)

LONG_DESCRIPTION = """
Generate synthetic defect images with NVIDIA Cosmos AnomalyGen.

Given a clean (defect-free) image, a placement mask, and an anomaly type
(e.g. `wood+crack`), the model inpaints a realistic defect into the mask's
region. Fine-tuned per-defect checkpoints load through the same model id /
local package path as the base model. Useful for bootstrapping defect
detection datasets where real defect data is scarce.

The placement mask comes in as an instance segmentation prediction - draw it
upstream (e.g. a polygon zone converted to detections) or chain a
segmentation model.

Alongside the generated image the block reports `visibility` - the mean
absolute pixel change inside the placement mask (0-255 gray levels). The
model sometimes returns the canvas unchanged; filter or regenerate when
visibility is low (>=15 separated real defects from empty generations in
practice).
"""


class BlockManifest(WorkflowBlockManifest):
model_config = ConfigDict(
json_schema_extra={
"name": "Cosmos AnomalyGen",
"version": "v1",
"short_description": "Inpaint realistic synthetic defects into clean images.",
"long_description": LONG_DESCRIPTION,
"license": "Other",
"block_type": "model",
"search_keywords": [
"Cosmos",
"AnomalyGen",
"NVIDIA",
"defect",
"synthetic data",
"inpainting",
"anomaly",
],
"ui_manifest": {
"section": "model",
"icon": "fal fa-hammer-crash",
"blockPriority": 5.9,
},
},
protected_namespaces=(),
)
type: Literal["roboflow_core/cosmos_anomalygen@v1"]

image: Selector(kind=[IMAGE_KIND]) = Field(
description="The clean (defect-free) image to inpaint a defect into.",
examples=["$inputs.image"],
)
segmentation_mask: Selector(kind=[INSTANCE_SEGMENTATION_PREDICTION_KIND]) = Field(
name="Placement Mask",
description="Segmentation prediction marking where the defect should appear.",
examples=["$steps.model.predictions"],
)
anomaly_type: Union[Selector(kind=[STRING_KIND]), str] = Field(
description="The trained anomaly type to generate, as `<texture>+<defect_class>`.",
examples=["wood+crack", "$inputs.anomaly_type"],
)
model_version: Union[Selector(kind=[ROBOFLOW_MODEL_ID_KIND]), str] = Field(
default="cosmos-anomalygen",
description="The Cosmos AnomalyGen checkpoint to use (model id or local package path).",
examples=["cosmos-anomalygen"],
)
guidance: Union[Selector(kind=[FLOAT_KIND]), float] = Field(
default=1.5,
description=(
"Anomaly conditioning strength (upstream extrapolation scale; 1.5 "
"corresponds to a standard classifier-free-guidance scale of 2.5 and "
"is NVIDIA's production default). Higher values make defects more "
"pronounced."
),
examples=[1.5],
)
num_steps: Union[Selector(kind=[INTEGER_KIND]), int] = Field(
default=35,
description="Number of denoising steps.",
examples=[35],
)
seed: Union[Selector(kind=[INTEGER_KIND]), int] = Field(
default=0,
description="Random seed for reproducible generation.",
examples=[0],
)
crop_ratio: Union[Selector(kind=[FLOAT_KIND]), float] = Field(
default=4.0,
description=(
"Size of the generation window relative to the mask's bounding box. "
"Larger values give the model more context but resample a larger "
"region; for defects bigger than ~1/4 of the frame a smaller ratio "
"keeps the surrounding image sharp."
),
examples=[4.0],
)
poisson_blend: Union[Selector(kind=[BOOLEAN_KIND]), bool] = Field(
default=False,
description="Poisson-blend the generated region into the original image.",
examples=[False],
)

@classmethod
def describe_outputs(cls) -> List[OutputDefinition]:
return [
OutputDefinition(
name="image",
kind=[IMAGE_KIND],
description="The clean image with the synthetic defect inpainted.",
),
OutputDefinition(
name="visibility",
kind=[FLOAT_KIND],
description=(
"Mean absolute pixel change inside the placement mask "
"(0-255). Low values mean the model returned the canvas "
"nearly unchanged; >=15 separated real defects from empty "
"generations in practice."
),
),
]

@classmethod
def get_execution_engine_compatibility(cls) -> Optional[str]:
return ">=1.3.0,<2.0.0"

@classmethod
def get_restrictions(cls) -> List[RuntimeRestriction]:
return [
RuntimeRestriction(
severity=Severity.HARD,
note="Requires a GPU; the diffusion model needs CUDA.",
applies_to_runtimes=[Runtime.SELF_HOSTED_CPU],
applies_to_step_execution_modes=[StepExecutionMode.LOCAL],
),
RuntimeRestriction(
severity=Severity.HARD,
note=(
"Cosmos AnomalyGen has no remote endpoint - the block loads "
"the model in-process and only supports local execution."
),
applies_to_runtimes=[Runtime.HOSTED_SERVERLESS],
applies_to_step_execution_modes=[StepExecutionMode.REMOTE],
),
]

@classmethod
def get_supported_model_variants(cls) -> Optional[List[str]]:
return ["cosmos-anomalygen"]

def discover_dependent_resources(self) -> Optional[List[DependentResource]]:
return [roboflow_platform_model(model_id=self.model_version)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium/High — declaring a model-manager dependency for a model this block loads via AutoModel, contradicting the sam2_video/sam3_video policy the block claims to follow.

This block loads its weights with AutoModel.from_pretrained(...) in _resolve_model() (in-process, not through the model manager) — exactly like segment_anything2_video/v1.py. Those blocks deliberately do not implement discover_dependent_resources():

# segment_anything2_video/v1.py
# `discover_dependent_resources()` deliberately not implemented: this
# block loads its weights via AutoModel.from_pretrained, not the model
# manager — dependencies stay undeclared (None) for now.

and are listed in NON_MODEL_MANAGER_LOADERS_ALLOWLIST in tests/workflows/unit_tests/core_steps/test_dependent_resources.py.

By returning roboflow_platform_model(model_id=...) here (defaulting to required_action=EXECUTIONexecution_location=ENVIRONMENT_DEFINED), this block opts into model-manager pre-loading. Concrete failure path: an InferencePipeline initialized with workflows_dependencies_pre_init=["roboflow_platform_model"] under LOCAL step execution → _is_locally_executed_platform_model() returns True (EXECUTION + ENVIRONMENT_DEFINED + LOCAL) → _pre_load_roboflow_platform_models() calls model_manager.add_model(model_id="cosmos-anomalygen", ...) (inference/core/workflows/execution_engine/v1/core.py:191). But cosmos-anomalygen is served only through the inference_models AutoModel path (custom backend, generate() contract) — it is not a model-manager model, so add_model registers the wrong thing / fails, and the block never uses that registration at runtime anyway (it re-loads via AutoModel).

Note CI is green here: test_every_block_with_resource_kind_fields_declares_dependencies only checks that a roboflow_model_id-field block either overrides discover_dependent_resources() or is allowlisted — so declaring the override silences the guard without making the pre-load path correct.

Recommend matching the cited precedent: drop this override (leave dependencies undeclared / None) and add roboflow_core/cosmos_anomalygen@v1 to NON_MODEL_MANAGER_LOADERS_ALLOWLIST. If instead the intent is genuine model-manager execution, the block must actually run through the model manager rather than AutoModel.

Reviewed at HEAD: 456946e



class CosmosAnomalyGenBlockV1(WorkflowBlock):
def __init__(
self,
api_key: Optional[str],
step_execution_mode: StepExecutionMode,
):
if step_execution_mode is not StepExecutionMode.LOCAL:
raise NotImplementedError(
"Cosmos AnomalyGen only supports local execution - there is no "
"remote endpoint for this model."
)
self._api_key = api_key
self._step_execution_mode = step_execution_mode
self._model = None
self._current_model_id: Optional[str] = None

@classmethod
def get_init_parameters(cls) -> List[str]:
return ["api_key", "step_execution_mode"]

@classmethod
def get_manifest(cls) -> Type[WorkflowBlockManifest]:
return BlockManifest

def run(
self,
image: WorkflowImageData,
segmentation_mask: sv.Detections,
anomaly_type: str,
model_version: str,
guidance: float,
num_steps: int,
seed: int,
crop_ratio: float,
poisson_blend: bool,
) -> BlockResult:
model = self._resolve_model(model_id=model_version)
numpy_image = image.numpy_image
mask = rasterize_placement_mask(
image=numpy_image, segmentation_mask=segmentation_mask
)
generated = model.generate(
image=numpy_image,
mask=mask,
anomaly_type=anomaly_type,
guidance=guidance,
num_steps=num_steps,
seed=seed,
num_images=1,
crop_ratio=crop_ratio,
poisson_blend=poisson_blend,
)
return {
"image": WorkflowImageData.copy_and_replace(
origin_image_data=image,
numpy_image=generated[0],
),
"visibility": compute_visibility(
original=numpy_image, generated=generated[0], mask=mask
),
}

def _resolve_model(self, model_id: str):
if self._model is None or self._current_model_id != model_id:
from inference_models import AutoModel

extra_weights_provider_headers = get_extra_weights_provider_headers()
self._model = AutoModel.from_pretrained(
model_id_or_path=model_id,
api_key=self._api_key,
allow_untrusted_packages=ALLOW_INFERENCE_MODELS_UNTRUSTED_PACKAGES,
allow_direct_local_storage_loading=ALLOW_INFERENCE_MODELS_DIRECTLY_ACCESS_LOCAL_PACKAGES,
weights_provider_extra_headers=extra_weights_provider_headers,
)
self._current_model_id = model_id
return self._model


def rasterize_placement_mask(
image: np.ndarray,
segmentation_mask: sv.Detections,
) -> np.ndarray:
black_image = np.zeros_like(image)
mask_annotator = sv.MaskAnnotator(color=Color.WHITE, opacity=1.0)
mask = mask_annotator.annotate(black_image, segmentation_mask)
return cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)


def compute_visibility(
original: np.ndarray,
generated: np.ndarray,
mask: np.ndarray,
) -> float:
"""Mean absolute gray-level change (0-255) inside the placement mask.

The model sometimes returns the canvas unchanged; this is the measure
callers filter or regenerate on.
"""
original_gray = cv2.cvtColor(original, cv2.COLOR_BGR2GRAY).astype(np.float32)
generated_gray = cv2.cvtColor(generated, cv2.COLOR_BGR2GRAY).astype(np.float32)
inside = mask >= 128
if not inside.any():
return 0.0
return float(np.abs(generated_gray - original_gray)[inside].mean())
Loading
Loading