Skip to content

Commit 780aca6

Browse files
svcnvidia-nemo-cithomasdhcclaude
authored
feat(deps): make opencv optional to strip ffmpeg CVEs from [all] (#2216) (#2221)
* feat(deps): make opencv-python-headless optional (new extra) * refactor(deps): tighten cv2 extra placement in pyproject * feat(deps): make vllm optional and drop from [all] * fix(bench): install cv2/vllm at runtime per benchmark modality * test: drop pytest.importorskip(cv2) from cv2-touching tests * fix(deps): strip opencv from image instead of dropping vllm from [all] * fix(docker): remove vllm import smoke test; tolerate opencv absence on arm64 --------- Signed-off-by: Dong Hyuk Chang <9426164+thomasdhc@users.noreply.github.com> Signed-off-by: NeMo Bot <nemo-bot@nvidia.com> Co-authored-by: Dong Hyuk Chang <9426164+thomasdhc@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bc7fdd2 commit 780aca6

13 files changed

Lines changed: 116 additions & 22 deletions

File tree

benchmarking/tools/ci_benchmark_launcher.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ fi
3434
cd /opt/Curator
3535
uv pip install GitPython pynvml pyyaml rich
3636

37+
# cv2 stripped from image (CVE removal); reinstall for benchmarks that need it.
38+
case "${ENTRY_NAME}" in
39+
interleaved_*|multimodal_*|video_*)
40+
uv pip install ".[cv2]"
41+
;;
42+
esac
43+
3744
# Session name resolution:
3845
# - If NEMO_CI_SESSION_NAME is set by the generated benchmark pipeline, use it
3946
# verbatim so every benchmark job writes to the same session directory.

docker/Dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ RUN if [ -n "${CURATOR_EXTRA}" ]; then \
9797
find /opt/venv -name "ray_dist.jar" -delete && \
9898
if find /opt/venv -name "ray_dist.jar" | grep -q .; then \
9999
echo "ERROR: GHSA-72hv-8253-57qq not fixed — ray_dist.jar still present after deletion" && exit 1; \
100-
fi
100+
fi && \
101+
# Strip opencv-python-headless (ffmpeg CVE carrier); tolerate arm64 where vllm doesn't pull it.
102+
(uv pip uninstall opencv-python-headless || true)
101103

102104
# Install nemotron-ocr (C++/CUDA extension — requires no-build-isolation)
103105
# GIT_LFS_SKIP_SMUDGE=1: clone source only, model weights are mounted at runtime

nemo_curator/stages/interleaved/filter/blur_filter.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
from dataclasses import dataclass
1818
from typing import TYPE_CHECKING
1919

20-
import cv2
2120
import pandas as pd
2221
from loguru import logger
2322

2423
from nemo_curator.stages.interleaved.stages import BaseInterleavedFilterStage
2524
from nemo_curator.stages.interleaved.utils import image_bytes_to_array
2625

26+
try:
27+
import cv2
28+
except ImportError:
29+
cv2 = None # type: ignore[assignment]
30+
2731
if TYPE_CHECKING:
2832
from collections.abc import Hashable
2933

@@ -32,10 +36,16 @@
3236
from nemo_curator.tasks import InterleavedBatch
3337

3438
DEFAULT_BLUR_SCORE_THRESHOLD: float = 100.0
39+
_CV2_INSTALL_HINT = (
40+
"opencv-python-headless is required for the interleaved blur filter. "
41+
"Install with: pip install nemo_curator[cv2]"
42+
)
3543

3644

3745
def _sharpness_score(image: np.ndarray, row_index: Hashable | None = None) -> float:
3846
"""Compute Laplacian variance as sharpness score; higher is sharper."""
47+
if cv2 is None:
48+
raise ImportError(_CV2_INSTALL_HINT)
3949
try:
4050
return float(cv2.Laplacian(image, cv2.CV_64F).var())
4151
except cv2.error as e:

nemo_curator/stages/interleaved/filter/qrcode_filter.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,34 @@
1717
from dataclasses import dataclass
1818
from typing import TYPE_CHECKING
1919

20-
import cv2
2120
import numpy as np
2221
import pandas as pd
2322
from loguru import logger
2423

2524
from nemo_curator.stages.interleaved.stages import BaseInterleavedFilterStage
2625
from nemo_curator.stages.interleaved.utils import image_bytes_to_array
2726

27+
try:
28+
import cv2
29+
except ImportError:
30+
cv2 = None # type: ignore[assignment]
31+
2832
if TYPE_CHECKING:
2933
from collections.abc import Hashable
3034

3135
from nemo_curator.tasks import InterleavedBatch
3236

3337
DEFAULT_QRCODE_SCORE_THRESHOLD: float = 0.05
38+
_CV2_INSTALL_HINT = (
39+
"opencv-python-headless is required for the interleaved QR code filter. "
40+
"Install with: pip install nemo_curator[cv2]"
41+
)
3442

3543

3644
def _qr_code_ratio(image: np.ndarray, row_index: Hashable | None = None) -> float:
3745
"""Return the ratio of image area covered by all detected QR code(s), in [0, 1]."""
46+
if cv2 is None:
47+
raise ImportError(_CV2_INSTALL_HINT)
3848
img_shape = image.shape
3949
height, width = img_shape[:2]
4050
img_area = float(height * width)

nemo_curator/stages/interleaved/pdf/nemotron_parse/utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232

3333
DEFAULT_MIN_CROP_PX = 10
3434
DEFAULT_MAX_PAGES = 50
35+
_CV2_INSTALL_HINT = (
36+
"opencv-python-headless is required for Nemotron-Parse PDF processing. "
37+
"Install with: pip install nemo_curator[cv2]"
38+
)
3539

3640

3741
def _render_scale_to_fit(page: Any, base_scale: float, max_wh: tuple[int, int] | None) -> float: # noqa: ANN401
@@ -56,7 +60,10 @@ def _render_scale_to_fit(page: Any, base_scale: float, max_wh: tuple[int, int] |
5660

5761
def _bitmap_to_rgb(bitmap: Any) -> Image.Image: # noqa: ANN401
5862
"""Convert a pypdfium2 bitmap to an RGB PIL image using OpenCV."""
59-
import cv2
63+
try:
64+
import cv2
65+
except ImportError as e:
66+
raise ImportError(_CV2_INSTALL_HINT) from e
6067

6168
arr = bitmap.to_numpy().copy()
6269
mode = bitmap.mode
@@ -173,7 +180,10 @@ def build_canvas(page_img: Image.Image, proc_size: tuple[int, int]) -> Image.Ima
173180
174181
This lets us crop bboxes directly in the model's coordinate space.
175182
"""
176-
import cv2
183+
try:
184+
import cv2
185+
except ImportError as e:
186+
raise ImportError(_CV2_INSTALL_HINT) from e
177187
import numpy as np
178188

179189
proc_h, proc_w = proc_size

nemo_curator/stages/interleaved/utils/image_utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,27 @@
1616

1717
from typing import TYPE_CHECKING
1818

19-
import cv2
2019
import numpy as np
2120
from loguru import logger
2221

22+
try:
23+
import cv2
24+
except ImportError:
25+
cv2 = None # type: ignore[assignment]
26+
2327
if TYPE_CHECKING:
2428
from collections.abc import Hashable
2529

30+
_CV2_INSTALL_HINT = (
31+
"opencv-python-headless is required for cv2-based image decoding. "
32+
"Install with: pip install nemo_curator[cv2]"
33+
)
34+
2635

2736
def image_bytes_to_array(image_bytes: bytes, *, row_index: Hashable | None = None) -> np.ndarray | None:
2837
"""Decode image bytes to RGB numpy array for OpenCV."""
38+
if cv2 is None:
39+
raise ImportError(_CV2_INSTALL_HINT)
2940
try:
3041
arr = np.frombuffer(image_bytes, dtype=np.uint8)
3142
image = cv2.imdecode(arr, cv2.IMREAD_COLOR)

nemo_curator/stages/text/embedders/vllm.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@
1818

1919
import torch
2020
from huggingface_hub import snapshot_download
21-
from vllm import LLM
21+
22+
try:
23+
from vllm import LLM
24+
25+
VLLM_AVAILABLE = True
26+
except ImportError:
27+
VLLM_AVAILABLE = False
28+
29+
class LLM: # dummy for type hints
30+
pass
31+
2232

2333
from nemo_curator.backends.base import NodeInfo, WorkerMetadata
2434
from nemo_curator.stages.base import ProcessingStage
@@ -29,6 +39,11 @@
2939
if TYPE_CHECKING:
3040
from transformers import AutoTokenizer
3141

42+
_VLLM_INSTALL_HINT = (
43+
"vLLM is required for VLLMEmbeddingModelStage. "
44+
"Install with: pip install nemo_curator[vllm]"
45+
)
46+
3247

3348
class VLLMEmbeddingModelStage(ProcessingStage[DocumentBatch, DocumentBatch]):
3449
def __init__( # noqa: PLR0913
@@ -79,6 +94,8 @@ def _initialize_vllm(self, local_files_only: bool) -> None:
7994
to its config resolution code — passing a repo ID with a custom cache dir
8095
fails offline.
8196
"""
97+
if not VLLM_AVAILABLE:
98+
raise ImportError(_VLLM_INSTALL_HINT)
8299
model_path = snapshot_download(
83100
self.model_identifier,
84101
cache_dir=self.cache_dir,

nemo_curator/stages/video/filtering/motion_vector_backend.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@
1717
from typing import cast
1818

1919
import av
20-
import cv2
2120
import numpy.typing as npt
2221
import torch
2322
from numpy.lib.recfunctions import structured_to_unstructured
2423
from torch import Tensor
2524

25+
try:
26+
import cv2
27+
except ImportError:
28+
cv2 = None # type: ignore[assignment]
29+
2630
# We error on any video with a width or height less than this.
2731
# The motion detection algorithm can't handle any resolutions less than this.
2832
_MIN_SIDE_RESOLUTION = 256
33+
_CV2_INSTALL_HINT = (
34+
"opencv-python-headless is required for the motion vector backend. "
35+
"Install with: pip install nemo_curator[cv2]"
36+
)
2937

3038

3139
class VideoResolutionTooSmallError(Exception):
@@ -270,6 +278,8 @@ def check_if_small_motion( # noqa: PLR0913
270278
MotionInfo object containing the results of the motion detection.
271279
272280
"""
281+
if cv2 is None:
282+
raise ImportError(_CV2_INSTALL_HINT)
273283
device = torch.device("cuda" if use_gpu else "cpu")
274284

275285
global_sum_tensor = torch.tensor(0.0, device=device)

nemo_curator/utils/decoder_utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,24 @@
2828
)
2929

3030
import av
31-
import cv2
3231
import numpy as np
3332
import numpy.typing as npt
3433

34+
try:
35+
import cv2
36+
except ImportError:
37+
cv2 = None # type: ignore[assignment]
38+
3539
if TYPE_CHECKING:
3640
from av.container import InputContainer
3741

3842
from nemo_curator.utils.operation_utils import make_pipeline_named_temporary_file
3943

44+
_CV2_INSTALL_HINT = (
45+
"opencv-python-headless is required for target-resolution resizing in extract_frames. "
46+
"Install with: pip install nemo_curator[cv2]"
47+
)
48+
4049

4150
class Resolution(NamedTuple):
4251
"""Container for video frame dimensions.
@@ -659,6 +668,8 @@ def extract_frames( # noqa: PLR0913
659668
)
660669

661670
if target_res[0] > 0 and target_res[1] > 0:
671+
if cv2 is None:
672+
raise ImportError(_CV2_INSTALL_HINT)
662673
interpolation = cv2.INTER_CUBIC
663674
frames = np.array(
664675
[cv2.resize(frame, (target_res[1], target_res[0]), interpolation=interpolation) for frame in frames]

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ cuda12 = [
7878
"gpustat",
7979
"nvidia-ml-py",
8080
]
81+
# Opt-in cv2 features; kept off `[all]` — wheel vendors CVE-flagged ffmpeg.
82+
cv2 = [
83+
"opencv-python-headless",
84+
]
8185
vllm = ["vllm>=0.14.1; (platform_machine == 'x86_64' and platform_system != 'Darwin')"]
8286

8387
# Inference Server (Ray Serve + vLLM) - for serving LLMs alongside Curator pipelines
@@ -218,7 +222,6 @@ text_cuda12 = [
218222
# Video Curation Dependencies
219223
video_cpu = [
220224
"av==17.1.0", # ffmpeg 8.1.1 (CVE: was 7.0.2)
221-
"opencv-python-headless", # dedupe with vllm's headless
222225
"torchvision",
223226
"einops",
224227
"easydict",
@@ -257,7 +260,6 @@ math_cuda12 = [
257260
interleaved_cpu = [
258261
"matplotlib",
259262
"open_clip_torch",
260-
"opencv-python-headless", # dedupe with vllm's headless
261263
"Pillow",
262264
"pypdfium2",
263265
"s3fs>=2024.12.0",

0 commit comments

Comments
 (0)