diff --git a/comfy_api/latest/__init__.py b/comfy_api/latest/__init__.py index 8542a1dbc..bca7628b7 100644 --- a/comfy_api/latest/__init__.py +++ b/comfy_api/latest/__init__.py @@ -6,8 +6,8 @@ from comfy_api.internal import ComfyAPIBase from comfy_api.internal.singleton import ProxiedSingleton from comfy_api.internal.async_to_sync import create_sync_class from ._input import ImageInput, AudioInput, MaskInput, LatentInput, VideoInput -from ._input_impl import VideoFromFile, VideoFromComponents -from ._util import VideoCodec, VideoContainer, VideoComponents, MESH, VOXEL, File3D +from ._input_impl import VideoFromFile, VideoFromComponents, File3D +from ._util import VideoCodec, VideoContainer, VideoComponents, MESH, VOXEL from . import _io_public as io from . import _ui_public as ui from comfy_execution.utils import get_executing_context @@ -98,6 +98,7 @@ class Input: class InputImpl: VideoFromFile = VideoFromFile VideoFromComponents = VideoFromComponents + File3D = File3D class Types: VideoCodec = VideoCodec diff --git a/comfy_api/latest/_input_impl/__init__.py b/comfy_api/latest/_input_impl/__init__.py index 02901b8b9..76ea889ac 100644 --- a/comfy_api/latest/_input_impl/__init__.py +++ b/comfy_api/latest/_input_impl/__init__.py @@ -1,7 +1,9 @@ from .video_types import VideoFromFile, VideoFromComponents +from .file3d_types import File3D __all__ = [ # Implementations "VideoFromFile", "VideoFromComponents", + "File3D", ] diff --git a/comfy_api/latest/_input_impl/file3d_types.py b/comfy_api/latest/_input_impl/file3d_types.py new file mode 100644 index 000000000..cd0a0a23c --- /dev/null +++ b/comfy_api/latest/_input_impl/file3d_types.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import io +import shutil +from pathlib import Path +from typing import IO, Union + + +class File3D: + """ + Class representing a 3D file from a file path or binary stream. + + Supports both disk-backed (file path) and memory-backed (BytesIO) storage, + similar to VideoFromFile. Disk-backed mode is more memory-efficient for + large 3D models. + """ + + def __init__(self, path: Union[str, IO[bytes]], file_format: str = ""): + """ + Initialize the File3D object. + + Args: + path: Either a file path (str) or a binary stream (BytesIO/IO[bytes]) + containing the 3D file data. + file_format: The format of the 3D file (e.g., 'glb', 'fbx', 'obj'). + If not provided and path is a string, it will be inferred + from the file extension. + """ + self._path = path + self._format = file_format or self._infer_format() + + def _infer_format(self) -> str: + """Infer file format from path if it's a string.""" + if isinstance(self._path, str): + return Path(self._path).suffix.lstrip(".").lower() + return "" + + @property + def format(self) -> str: + """Get the file format.""" + return self._format + + @format.setter + def format(self, value: str) -> None: + """Set the file format.""" + self._format = value.lstrip(".").lower() if value else "" + + @property + def is_disk_backed(self) -> bool: + """Check if the file is stored on disk (vs in memory).""" + return isinstance(self._path, str) + + def get_source(self) -> Union[str, IO[bytes]]: + """ + Get the underlying source for streaming. + + Returns: + Either a file path (str) or a BytesIO object. + For disk-backed files, returns the path directly to avoid memory copy. + """ + if isinstance(self._path, str): + return self._path + # Reset stream position for IO objects + if hasattr(self._path, "seek"): + self._path.seek(0) + return self._path + + @property + def data(self) -> io.BytesIO: + """ + Get the file data as a BytesIO object. + + For disk-backed files, this reads the entire file into memory. + The returned BytesIO is positioned at the beginning. + """ + if isinstance(self._path, str): + # Read from disk + with open(self._path, "rb") as f: + result = io.BytesIO(f.read()) + return result + # Already a stream - reset position and return + if hasattr(self._path, "seek"): + self._path.seek(0) + if isinstance(self._path, io.BytesIO): + return self._path + # For other IO types, wrap in BytesIO + return io.BytesIO(self._path.read()) + + def save_to(self, path: str) -> str: + """ + Save the 3D file to disk. + + For disk-backed files, this uses an efficient file copy. + For memory-backed files, this writes the buffer to disk. + + Args: + path: Destination file path. + + Returns: + The destination path. + """ + dest = Path(path) + dest.parent.mkdir(parents=True, exist_ok=True) + + if isinstance(self._path, str): + # Disk-backed: efficient copy + if Path(self._path).resolve() != dest.resolve(): + shutil.copy2(self._path, dest) + else: + # Memory-backed: write bytes + if hasattr(self._path, "seek"): + self._path.seek(0) + with open(dest, "wb") as f: + f.write(self._path.read()) + + return str(dest) + + def get_bytes(self) -> bytes: + """ + Get the raw bytes of the 3D file. + + For disk-backed files, this reads the entire file. + For memory-backed files, this returns the buffer contents. + """ + if isinstance(self._path, str): + return Path(self._path).read_bytes() + if hasattr(self._path, "seek"): + self._path.seek(0) + return self._path.read() + + def __repr__(self) -> str: + if isinstance(self._path, str): + return f"File3D(path={self._path!r}, format={self._format!r})" + return f"File3D(, format={self._format!r})" diff --git a/comfy_api/latest/_io.py b/comfy_api/latest/_io.py index 0558274b5..99a5e632e 100644 --- a/comfy_api/latest/_io.py +++ b/comfy_api/latest/_io.py @@ -27,7 +27,8 @@ if TYPE_CHECKING: from comfy_api.internal import (_ComfyNodeInternal, _NodeOutputInternal, classproperty, copy_class, first_real_override, is_class, prune_dict, shallow_clone_class) from comfy_execution.graph_utils import ExecutionBlocker -from ._util import MESH, VOXEL, File3D, SVG as _SVG +from ._util import MESH, VOXEL, SVG as _SVG +from ._input_impl import File3D class FolderType(str, Enum): diff --git a/comfy_api/latest/_util/__init__.py b/comfy_api/latest/_util/__init__.py index 115baf392..6313eb01b 100644 --- a/comfy_api/latest/_util/__init__.py +++ b/comfy_api/latest/_util/__init__.py @@ -1,5 +1,5 @@ from .video_types import VideoContainer, VideoCodec, VideoComponents -from .geometry_types import VOXEL, MESH, File3D +from .geometry_types import VOXEL, MESH from .image_types import SVG __all__ = [ @@ -9,6 +9,5 @@ __all__ = [ "VideoComponents", "VOXEL", "MESH", - "File3D", "SVG", ] diff --git a/comfy_api/latest/_util/geometry_types.py b/comfy_api/latest/_util/geometry_types.py index 1e986544c..ab52a54b7 100644 --- a/comfy_api/latest/_util/geometry_types.py +++ b/comfy_api/latest/_util/geometry_types.py @@ -1,5 +1,3 @@ -from io import BytesIO - import torch @@ -14,28 +12,7 @@ class MESH: self.faces = faces -class File3D: - """3D file type storing binary data in memory. - - This is the backing class for all FILE_3D_* ComfyTypes. - """ - - def __init__(self, data: BytesIO, file_format: str): - self._data = data - self.format = file_format - - @property - def data(self) -> BytesIO: - """Get the BytesIO data, seeking to the beginning.""" - self._data.seek(0) - return self._data - - def save_to(self, path: str) -> str: - """Save the 3D file data to disk.""" - self._data.seek(0) - with open(path, "wb") as f: - f.write(self._data.read()) - return path - - def __repr__(self) -> str: - return f"File3D({self.format})" +__all__ = [ + "VOXEL", + "MESH", +] diff --git a/comfy_api_nodes/util/download_helpers.py b/comfy_api_nodes/util/download_helpers.py index daf44aa8b..85b371e40 100644 --- a/comfy_api_nodes/util/download_helpers.py +++ b/comfy_api_nodes/util/download_helpers.py @@ -296,4 +296,4 @@ async def download_url_to_file_3d( output_path.write_bytes(data.getvalue()) data.seek(0) - return Types.File3D(data=data, file_format=file_format) + return Types.File3D(path=data, file_format=file_format)