diff --git a/README.md b/README.md index 2ec30cb..1802240 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ python ComfyUI-SAM-Audio/install.py Then restart ComfyUI. If installed through ComfyUI Manager, the installer should run automatically. +The node uses ComfyUI's decoded media tensors and does not install or load TorchCodec. + ## Model download Select a model in **SAM-Audio Model Loader** and queue the workflow. The loader first tries Meta's official repository. If Hugging Face denies gated access, it automatically downloads the matching pinned and checksum-verified checkpoint from the [public mirror collection](https://huggingface.co/collections/mrfakename/sam-audio). diff --git a/pyproject.toml b/pyproject.toml index 7b229aa..9ab841a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "regex", "safetensors", "timm", - "torchcodec", "torchdiffeq", "tqdm", "transformers>=4.54.0", diff --git a/requirements.txt b/requirements.txt index d1e0059..5e0d28c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,6 @@ huggingface-hub regex safetensors timm -torchcodec torchdiffeq tqdm transformers>=4.54.0 diff --git a/sam_audio_comfy/runtime.py b/sam_audio_comfy/runtime.py index 0b98e44..8541263 100644 --- a/sam_audio_comfy/runtime.py +++ b/sam_audio_comfy/runtime.py @@ -13,6 +13,7 @@ import torch from .attention import validate_attention_backend from .audiotools_compat import audiotools_import_compatibility +from .torchcodec_compat import torchcodec_import_compatibility from .xformers_compat import xformers_import_compatibility LOGGER = logging.getLogger(__name__) @@ -305,14 +306,17 @@ def import_sam_audio() -> tuple[Any, Any]: LOGGER.info( "Loading SAM-Audio through isolated inference-only compatibility layers" ) - with xformers_import_compatibility(), audiotools_import_compatibility(): + with ( + xformers_import_compatibility(), + audiotools_import_compatibility(), + torchcodec_import_compatibility(), + ): module = importlib.import_module("sam_audio") return module.SAMAudio, module.SAMAudioProcessor except Exception as error: raise RuntimeError( "SAM-Audio could not be imported. Install this node's requirements with " - "ComfyUI's Python interpreter, then restart ComfyUI. This node does not " - "require or install xFormers." + "ComfyUI's Python interpreter, then restart ComfyUI." ) from error diff --git a/sam_audio_comfy/torchcodec_compat.py b/sam_audio_comfy/torchcodec_compat.py new file mode 100644 index 0000000..71fe117 --- /dev/null +++ b/sam_audio_comfy/torchcodec_compat.py @@ -0,0 +1,64 @@ +"""Import-only TorchCodec surface for tensor-native ComfyUI inference.""" + +from __future__ import annotations + +import contextlib +import sys +import types +from collections.abc import Iterator + + +class _TensorInputsOnly: + def __init__(self, *_args, **_kwargs): + raise RuntimeError( + "TorchCodec file decoding is disabled in ComfyUI-SAM-Audio. Pass decoded " + "ComfyUI AUDIO, IMAGE, and MASK tensors to the node instead." + ) + + +class AudioDecoder(_TensorInputsOnly): + """Placeholder for an upstream file-I/O path unused by the ComfyUI node.""" + + +class VideoDecoder(_TensorInputsOnly): + """Placeholder for an upstream file-I/O path unused by the ComfyUI node.""" + + +class AudioEncoder(_TensorInputsOnly): + """Placeholder for an optional upstream ranker disabled by this integration.""" + + +def _compatibility_modules() -> dict[str, types.ModuleType]: + torchcodec = types.ModuleType("torchcodec") + torchcodec.__path__ = [] + decoders = types.ModuleType("torchcodec.decoders") + encoders = types.ModuleType("torchcodec.encoders") + decoders.AudioDecoder = AudioDecoder + decoders.VideoDecoder = VideoDecoder + encoders.AudioEncoder = AudioEncoder + torchcodec.decoders = decoders + torchcodec.encoders = encoders + return { + "torchcodec": torchcodec, + "torchcodec.decoders": decoders, + "torchcodec.encoders": encoders, + } + + +@contextlib.contextmanager +def torchcodec_import_compatibility() -> Iterator[None]: + """Temporarily provide upstream's unused decoder imports without loading FFmpeg.""" + compatibility_modules = _compatibility_modules() + missing = object() + previous = { + name: sys.modules.get(name, missing) for name in compatibility_modules + } + sys.modules.update(compatibility_modules) + try: + yield + finally: + for name, module in previous.items(): + if module is missing: + sys.modules.pop(name, None) + else: + sys.modules[name] = module diff --git a/tests/test_package.py b/tests/test_package.py index 36d688c..a281ded 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,9 +1,15 @@ import sys +import types from pathlib import Path +import pytest import torch -from sam_audio_comfy import audiotools_compat, xformers_compat +from sam_audio_comfy import ( + audiotools_compat, + torchcodec_compat, + xformers_compat, +) def test_node_does_not_require_xformers(): @@ -21,6 +27,17 @@ def test_node_does_not_require_xformers(): assert not any("xformers" in line for line in dependency_lines) +def test_node_does_not_require_torchcodec(): + root = Path(__file__).parents[1] + for filename in ("requirements.txt", "pyproject.toml"): + dependency_lines = [ + line + for line in (root / filename).read_text().lower().splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] + assert not any("torchcodec" in line for line in dependency_lines) + + def test_upstream_installer_uses_no_deps(): root = Path(__file__).parents[1] installer = (root / "install.py").read_text().lower() @@ -58,3 +75,33 @@ def test_audiotools_compatibility_is_import_only_and_scoped(monkeypatch): assert audiotools.ml.BaseModel.INTERN == [] assert "audiotools" not in sys.modules + + +def test_torchcodec_compatibility_is_import_only_and_scoped(monkeypatch): + monkeypatch.delitem(sys.modules, "torchcodec", raising=False) + monkeypatch.delitem(sys.modules, "torchcodec.decoders", raising=False) + monkeypatch.delitem(sys.modules, "torchcodec.encoders", raising=False) + + with torchcodec_compat.torchcodec_import_compatibility(): + decoders = sys.modules["torchcodec.decoders"] + assert decoders.AudioDecoder is torchcodec_compat.AudioDecoder + assert decoders.VideoDecoder is torchcodec_compat.VideoDecoder + with pytest.raises(RuntimeError, match="decoded ComfyUI"): + decoders.AudioDecoder("input.wav") + + assert "torchcodec" not in sys.modules + assert "torchcodec.decoders" not in sys.modules + + +def test_existing_torchcodec_modules_are_restored(monkeypatch): + original = types.ModuleType("torchcodec") + original_decoders = types.ModuleType("torchcodec.decoders") + monkeypatch.setitem(sys.modules, "torchcodec", original) + monkeypatch.setitem(sys.modules, "torchcodec.decoders", original_decoders) + + with torchcodec_compat.torchcodec_import_compatibility(): + assert sys.modules["torchcodec"] is not original + assert sys.modules["torchcodec.decoders"] is not original_decoders + + assert sys.modules["torchcodec"] is original + assert sys.modules["torchcodec.decoders"] is original_decoders