Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,9 @@ This node pack focuses on **image / video / edit** — see the full **[node cata
| AtlasCloud Seedance 2.0 Mini Reference-to-Video | bytedance/seedance-2.0-mini/reference-to-video |
| AtlasCloud Avatar Omni Human 1.5 | bytedance/avatar-omni-human-v1.5 |
| AtlasCloud Image Upscaler | atlascloud/image-upscaler |
| AtlasCloud Face Swap (Image) | atlascloud/face-swap-image |
| AtlasCloud Photo Cleanup | atlascloud/photo-cleanup |
| AtlasCloud Face Swap (Video) | atlascloud/face-swap-video |
| AtlasCloud Seedance 2.0 Reference-to-Video Upscaled | bytedance/seedance-2.0/reference-to-video-upscaled |
| AtlasCloud Seedance 2.0 Fast Reference-to-Video Upscaled | bytedance/seedance-2.0-fast/reference-to-video-upscaled |
| AtlasCloud Vidu Q3 Reference-to-Video | vidu/q3/reference-to-video |
Expand Down
82 changes: 82 additions & 0 deletions src/atlascloud_comfyui/nodes/image/atlascloud_face_swap_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

from typing import Any, Dict, Tuple

from ..auth.atlas_client_node import AtlasClientHandle


class AtlasFaceSwapImage:
CATEGORY = "AtlasCloud/Image"
FUNCTION = "run"
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("image_url", "prediction_id")

@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"atlas_client": ("ATLAS_CLIENT",),
"source_face": ("STRING", {"default": "", "tooltip": "Source Face — the face to transfer (clear front-facing photo)"}),
"target_image": ("STRING", {"default": "", "tooltip": "Target Image — the photo whose face gets replaced (pose/clothing/background preserved)"}),
},
"optional": {
"size": (
["auto", "2048*2048", "2304*1728", "1728*2304", "2720*1530", "1530*2720", "2496*1664", "1664*2496", "1024*1024", "1536*1536", "1776*1328", "1328*1776", "2048*1152", "1152*2048"],
{"default": "auto", "tooltip": "Output resolution/size"},
),
"output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "Output image format"}),
"poll_interval_sec": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 10.0, "tooltip": "Polling interval (seconds)"}),
"timeout_sec": ("INT", {"default": 300, "min": 30, "max": 7200, "tooltip": "Timeout (seconds)"}),
},
}

def run(
self,
atlas_client: AtlasClientHandle,
source_face: str,
target_image: str,
size: str = "auto",
output_format: str = "jpeg",
poll_interval_sec: float = 2.0,
timeout_sec: int = 300,
) -> Tuple[str, str]:
face = (source_face or "").strip()
if not face:
raise RuntimeError("source_face is required for AtlasCloud Face Swap (Image)")

target = (target_image or "").strip()
if not target:
raise RuntimeError("target_image is required for AtlasCloud Face Swap (Image)")

client = atlas_client.client

payload: Dict[str, Any] = {
"model": "atlascloud/face-swap-image",
"image": face,
"Image": target,
"size": size,
"output_format": output_format,
}

prediction_id = client.generate_image(payload)
result = client.poll_prediction(
prediction_id,
poll_interval_sec=float(poll_interval_sec),
timeout_sec=float(timeout_sec),
)

outputs = (result.get("data") or {}).get("outputs") or []
if not outputs:
raise RuntimeError(f"No outputs returned for prediction {prediction_id}: {result}")

first = outputs[0]
if isinstance(first, dict):
url = first.get("url") or first.get("image") or first.get("output")
if isinstance(url, str) and url.strip():
return (url, prediction_id)
raise RuntimeError(f"Unexpected output object for prediction {prediction_id}: {first}")

if not isinstance(first, str):
raise RuntimeError(f"Unexpected output type for prediction {prediction_id}: {type(first).__name__} {first!r}")

return (first, prediction_id)
69 changes: 69 additions & 0 deletions src/atlascloud_comfyui/nodes/image/atlascloud_photo_cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

from typing import Any, Dict, Tuple

from ..auth.atlas_client_node import AtlasClientHandle


class AtlasPhotoCleanup:
CATEGORY = "AtlasCloud/Image"
FUNCTION = "run"
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("image_url", "prediction_id")

@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"atlas_client": ("ATLAS_CLIENT",),
"image": ("STRING", {"default": "", "tooltip": "Input image URL to clean up (dust/scratches/noise removal)"}),
},
"optional": {
"output_format": (["png", "jpeg", "webp", "jpg"], {"default": "png", "tooltip": "Output image format"}),
"poll_interval_sec": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 10.0, "tooltip": "Polling interval (seconds)"}),
"timeout_sec": ("INT", {"default": 300, "min": 30, "max": 7200, "tooltip": "Timeout (seconds)"}),
},
}

def run(
self,
atlas_client: AtlasClientHandle,
image: str,
output_format: str = "png",
poll_interval_sec: float = 2.0,
timeout_sec: int = 300,
) -> Tuple[str, str]:
img = (image or "").strip()
if not img:
raise RuntimeError("image is required for AtlasCloud Photo Cleanup")

client = atlas_client.client

payload: Dict[str, Any] = {
"model": "atlascloud/photo-cleanup",
"image": img,
"output_format": output_format,
}

prediction_id = client.generate_image(payload)
result = client.poll_prediction(
prediction_id,
poll_interval_sec=float(poll_interval_sec),
timeout_sec=float(timeout_sec),
)

outputs = (result.get("data") or {}).get("outputs") or []
if not outputs:
raise RuntimeError(f"No outputs returned for prediction {prediction_id}: {result}")

first = outputs[0]
if isinstance(first, dict):
url = first.get("url") or first.get("image") or first.get("output")
if isinstance(url, str) and url.strip():
return (url, prediction_id)
raise RuntimeError(f"Unexpected output object for prediction {prediction_id}: {first}")

if not isinstance(first, str):
raise RuntimeError(f"Unexpected output type for prediction {prediction_id}: {type(first).__name__} {first!r}")

return (first, prediction_id)
66 changes: 66 additions & 0 deletions src/atlascloud_comfyui/nodes/video/atlascloud_face_swap_video.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

from typing import Any, Dict, Tuple

from ..auth.atlas_client_node import AtlasClientHandle


class AtlasFaceSwapVideo:
CATEGORY = "AtlasCloud/Video"
FUNCTION = "run"
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("video_url", "prediction_id")

@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"atlas_client": ("ATLAS_CLIENT",),
"source_face": ("STRING", {"default": "", "tooltip": "Source Face — clear front-facing photo of the face to place into the video"}),
"video": ("STRING", {"default": "", "tooltip": "Target Video — the video whose person will be replaced (MP4/MOV, 2-10s, <=100MB)"}),
},
"optional": {
"resolution": (["720P", "1080P"], {"default": "1080P", "tooltip": "Output video resolution"}),
"poll_interval_sec": ("FLOAT", {"default": 2.0, "min": 0.5, "max": 10.0, "tooltip": "Polling interval (seconds)"}),
"timeout_sec": ("INT", {"default": 900, "min": 30, "max": 7200, "tooltip": "Timeout (seconds)"}),
},
}

def run(
self,
atlas_client: AtlasClientHandle,
source_face: str,
video: str,
resolution: str = "1080P",
poll_interval_sec: float = 2.0,
timeout_sec: int = 900,
) -> Tuple[str, str]:
face = (source_face or "").strip()
if not face:
raise RuntimeError("source_face is required for AtlasCloud Face Swap (Video)")

vid = (video or "").strip()
if not vid:
raise RuntimeError("video is required for AtlasCloud Face Swap (Video)")

client = atlas_client.client

payload: Dict[str, Any] = {
"model": "atlascloud/face-swap-video",
"image": face,
"video": vid,
"resolution": resolution,
}

prediction_id = client.generate_video(payload)
result = client.poll_prediction(
prediction_id,
poll_interval_sec=float(poll_interval_sec),
timeout_sec=float(timeout_sec),
)

outputs = (result.get("data") or {}).get("outputs") or []
if not outputs:
raise RuntimeError(f"No outputs returned for prediction {prediction_id}: {result}")

return (outputs[0], prediction_id)
9 changes: 9 additions & 0 deletions src/atlascloud_comfyui/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@
from atlascloud_comfyui.nodes.video.bytedance_seedance_2_0_mini_r2v import AtlasSeedance20MiniReferenceToVideo
from atlascloud_comfyui.nodes.video.bytedance_avatar_omni_human_v15 import AtlasAvatarOmniHumanV15
from atlascloud_comfyui.nodes.image.atlascloud_image_upscaler import AtlasImageUpscaler
from atlascloud_comfyui.nodes.image.atlascloud_face_swap_image import AtlasFaceSwapImage
from atlascloud_comfyui.nodes.image.atlascloud_photo_cleanup import AtlasPhotoCleanup
from atlascloud_comfyui.nodes.video.atlascloud_face_swap_video import AtlasFaceSwapVideo
from atlascloud_comfyui.nodes.deprecated.video.bytedance_seedance_2_0_t2v_upscaled import AtlasSeedance20TextToVideoUpscaled
from atlascloud_comfyui.nodes.deprecated.video.bytedance_seedance_2_0_i2v_upscaled import AtlasSeedance20ImageToVideoUpscaled
from atlascloud_comfyui.nodes.deprecated.video.bytedance_seedance_2_0_r2v_upscaled import AtlasSeedance20ReferenceToVideoUpscaled
Expand Down Expand Up @@ -450,6 +453,9 @@
"AtlasCloud Seedance 2.0 Mini Reference-to-Video": AtlasSeedance20MiniReferenceToVideo,
"AtlasCloud Avatar Omni Human 1.5": AtlasAvatarOmniHumanV15,
"AtlasCloud Image Upscaler": AtlasImageUpscaler,
"AtlasCloud Face Swap (Image)": AtlasFaceSwapImage,
"AtlasCloud Photo Cleanup": AtlasPhotoCleanup,
"AtlasCloud Face Swap (Video)": AtlasFaceSwapVideo,
"AtlasCloud Seedance 2.0 Text-to-Video Upscaled": AtlasSeedance20TextToVideoUpscaled,
"AtlasCloud Seedance 2.0 Image-to-Video Upscaled": AtlasSeedance20ImageToVideoUpscaled,
"AtlasCloud Seedance 2.0 Reference-to-Video Upscaled": AtlasSeedance20ReferenceToVideoUpscaled,
Expand Down Expand Up @@ -795,6 +801,9 @@
"AtlasCloud Seedance 2.0 Mini Reference-to-Video": "AtlasCloud Seedance 2.0 Mini Reference-to-Video",
"AtlasCloud Avatar Omni Human 1.5": "AtlasCloud Avatar Omni Human 1.5",
"AtlasCloud Image Upscaler": "AtlasCloud Image Upscaler",
"AtlasCloud Face Swap (Image)": "AtlasCloud Face Swap (Image)",
"AtlasCloud Photo Cleanup": "AtlasCloud Photo Cleanup",
"AtlasCloud Face Swap (Video)": "AtlasCloud Face Swap (Video)",
"AtlasCloud Seedance 2.0 Text-to-Video Upscaled": "AtlasCloud Seedance 2.0 Text-to-Video Upscaled",
"AtlasCloud Seedance 2.0 Image-to-Video Upscaled": "AtlasCloud Seedance 2.0 Image-to-Video Upscaled",
"AtlasCloud Seedance 2.0 Reference-to-Video Upscaled": "AtlasCloud Seedance 2.0 Reference-to-Video Upscaled",
Expand Down
75 changes: 75 additions & 0 deletions tests/test_new_nodes_2026_07_16.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Metadata-only tests for newly added nodes (2026-07-16).

AtlasCloud tools: Face Swap (Image), Photo Cleanup, Face Swap (Video).
These tests MUST NOT require ATLASCLOUD_API_KEY.
"""


def test_face_swap_image_metadata():
from src.atlascloud_comfyui.nodes.image.atlascloud_face_swap_image import (
AtlasFaceSwapImage,
)

required = AtlasFaceSwapImage.INPUT_TYPES()["required"]
assert "atlas_client" in required
assert "source_face" in required
assert "target_image" in required
assert AtlasFaceSwapImage.RETURN_TYPES == ("STRING", "STRING")
assert AtlasFaceSwapImage.CATEGORY == "AtlasCloud/Image"


def test_photo_cleanup_metadata():
from src.atlascloud_comfyui.nodes.image.atlascloud_photo_cleanup import (
AtlasPhotoCleanup,
)

required = AtlasPhotoCleanup.INPUT_TYPES()["required"]
assert "atlas_client" in required
assert "image" in required
assert AtlasPhotoCleanup.RETURN_TYPES == ("STRING", "STRING")
assert AtlasPhotoCleanup.CATEGORY == "AtlasCloud/Image"


def test_face_swap_video_metadata():
from src.atlascloud_comfyui.nodes.video.atlascloud_face_swap_video import (
AtlasFaceSwapVideo,
)

required = AtlasFaceSwapVideo.INPUT_TYPES()["required"]
assert "atlas_client" in required
assert "source_face" in required
assert "video" in required
assert AtlasFaceSwapVideo.RETURN_TYPES == ("STRING", "STRING")
assert AtlasFaceSwapVideo.CATEGORY == "AtlasCloud/Video"


def test_new_nodes_2026_07_16_registered():
from src.atlascloud_comfyui.registry import (
NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS,
)

for key in (
"AtlasCloud Face Swap (Image)",
"AtlasCloud Photo Cleanup",
"AtlasCloud Face Swap (Video)",
):
assert key in NODE_CLASS_MAPPINGS
assert key in NODE_DISPLAY_NAME_MAPPINGS


def test_new_nodes_2026_07_16_model_ids():
import inspect

from src.atlascloud_comfyui.nodes.image.atlascloud_face_swap_image import AtlasFaceSwapImage
from src.atlascloud_comfyui.nodes.image.atlascloud_photo_cleanup import AtlasPhotoCleanup
from src.atlascloud_comfyui.nodes.video.atlascloud_face_swap_video import AtlasFaceSwapVideo

expected = {
AtlasFaceSwapImage: "atlascloud/face-swap-image",
AtlasPhotoCleanup: "atlascloud/photo-cleanup",
AtlasFaceSwapVideo: "atlascloud/face-swap-video",
}
for cls, model_id in expected.items():
src = inspect.getsource(cls.run)
assert f'"model": "{model_id}"' in src
Loading