From 90453ab6a7a2cba4a717627ab4272f43a74ceb0d Mon Sep 17 00:00:00 2001 From: Yanxi Chen <103548668+YC0101@users.noreply.github.com> Date: Fri, 3 Jul 2026 08:05:03 -0400 Subject: [PATCH] sync: add 4 AtlasCloud models (2026-07-03) - hidream-o1-1.5/text-to-image - hidream-o1-1.5/edit - sync/lipsync-v3 - veed/lipsync Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 + .../nodes/image/hidream_o1_15_edit.py | 87 +++++++++++++++++++ .../nodes/image/hidream_o1_15_t2i.py | 83 ++++++++++++++++++ .../nodes/video/sync_lipsync_v3.py | 65 ++++++++++++++ .../nodes/video/veed_lipsync.py | 59 +++++++++++++ src/atlascloud_comfyui/registry.py | 12 +++ tests/test_new_nodes_2026_07_03.py | 87 +++++++++++++++++++ 7 files changed, 397 insertions(+) create mode 100644 src/atlascloud_comfyui/nodes/image/hidream_o1_15_edit.py create mode 100644 src/atlascloud_comfyui/nodes/image/hidream_o1_15_t2i.py create mode 100644 src/atlascloud_comfyui/nodes/video/sync_lipsync_v3.py create mode 100644 src/atlascloud_comfyui/nodes/video/veed_lipsync.py create mode 100644 tests/test_new_nodes_2026_07_03.py diff --git a/README.md b/README.md index e53714a..ec4b0d0 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,8 @@ This node pack focuses on **image / video / edit** — see the full **[node cata | Node | Model | |------|-------| | AtlasCloud InfiniteTalk Audio-to-Video | atlascloud/infinitetalk | +| AtlasCloud Sync Lipsync v3 | sync/lipsync-v3 | +| AtlasCloud VEED Lipsync | veed/lipsync | ### Text-to-Image (T2I) @@ -354,6 +356,7 @@ This node pack focuses on **image / video / edit** — see the full **[node cata | AtlasCloud Nano Banana 2 Text-to-Image Developer | google/nano-banana-2/text-to-image-developer | | AtlasCloud Nano Banana 2 Lite Text-to-Image | google/nano-banana-2-lite/text-to-image | | AtlasCloud Nano Banana 2 Lite Text-to-Image Developer | google/nano-banana-2-lite/text-to-image-developer | +| AtlasCloud HiDream O1 1.5 Text-to-Image | hidream-o1-1.5/text-to-image | | AtlasCloud Nano Banana Pro Text-to-Image Ultra | google/nano-banana-pro/text-to-image-ultra | | AtlasCloud Nano Banana Pro Text-to-Image | google/nano-banana-pro/text-to-image | | AtlasCloud Nano Banana Pro Text-to-Image Developer | google/nano-banana-pro/text-to-image-developer | @@ -414,6 +417,7 @@ This node pack focuses on **image / video / edit** — see the full **[node cata | AtlasCloud Nano Banana 2 Edit Developer | google/nano-banana-2/edit-developer | | AtlasCloud Nano Banana 2 Lite Edit | google/nano-banana-2-lite/edit | | AtlasCloud Nano Banana 2 Lite Edit Developer | google/nano-banana-2-lite/edit-developer | +| AtlasCloud HiDream O1 1.5 Edit | hidream-o1-1.5/edit | | AtlasCloud Nano Banana 2 Reference-to-Image | google/nano-banana-2/reference-to-image | | AtlasCloud Nano Banana 2 Reference-to-Image Developer | google/nano-banana-2/reference-to-image-developer | | AtlasCloud Seedream V5.0 Lite Edit | bytedance/seedream-v5.0-lite/edit | diff --git a/src/atlascloud_comfyui/nodes/image/hidream_o1_15_edit.py b/src/atlascloud_comfyui/nodes/image/hidream_o1_15_edit.py new file mode 100644 index 0000000..5ab5b61 --- /dev/null +++ b/src/atlascloud_comfyui/nodes/image/hidream_o1_15_edit.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Tuple + +from ..auth.atlas_client_node import AtlasClientHandle + +_IMAGE_SIZES = [ + "square_hd", + "square", + "portrait_3_4", + "portrait_9_16", + "landscape_4_3", + "landscape_16_9", +] + + +class AtlasHiDreamO115Edit: + 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(s) or base64 to edit, ONE PER LINE"}), + "prompt": ("STRING", {"multiline": True, "tooltip": "Text prompt for editing (max 2500 chars)"}), + "image_size": (_IMAGE_SIZES, {"default": "landscape_4_3", "tooltip": "Output image size preset"}), + "num_inference_steps": ("INT", {"default": 50, "min": 1, "max": 100, "tooltip": "Denoising steps"}), + "guidance_scale": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 20.0, "tooltip": "Guidance scale"}), + "output_format": (["png", "jpeg", "webp"], {"default": "png", "tooltip": "Output format"}), + }, + "optional": { + "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, + prompt: str, + image_size: str, + num_inference_steps: int, + guidance_scale: float, + output_format: str, + poll_interval_sec: float = 2.0, + timeout_sec: int = 300, + ) -> Tuple[str, str]: + client = atlas_client.client + + p = (prompt or "").strip() + if not p: + raise RuntimeError("prompt is required") + + image = (image or "").strip() + if not image: + raise RuntimeError("image is required (URL or base64)") + + # Split on newlines, NOT commas: base64 data URLs contain a comma. + refs: List[str] = [u.strip() for u in image.splitlines() if u.strip()] + + payload: Dict[str, Any] = { + "model": "hidream-o1-1.5/edit", + "prompt": p, + "reference_image_urls": refs, + "image_size": image_size, + "num_inference_steps": int(num_inference_steps), + "guidance_scale": float(guidance_scale), + "output_format": output_format, + } + + prediction_id = client.generate_image(payload) + result = client.poll_prediction( + prediction_id, + poll_interval_sec=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) diff --git a/src/atlascloud_comfyui/nodes/image/hidream_o1_15_t2i.py b/src/atlascloud_comfyui/nodes/image/hidream_o1_15_t2i.py new file mode 100644 index 0000000..a74f613 --- /dev/null +++ b/src/atlascloud_comfyui/nodes/image/hidream_o1_15_t2i.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Tuple + +from ..auth.atlas_client_node import AtlasClientHandle + +_IMAGE_SIZES = [ + "square_hd", + "square", + "portrait_3_4", + "portrait_9_16", + "landscape_4_3", + "landscape_16_9", +] + + +class AtlasHiDreamO115TextToImage: + 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",), + "prompt": ("STRING", {"multiline": True, "tooltip": "Text prompt (max 2500 chars)"}), + "image_size": (_IMAGE_SIZES, {"default": "landscape_4_3", "tooltip": "Output image size preset"}), + "num_inference_steps": ("INT", {"default": 50, "min": 1, "max": 100, "tooltip": "Denoising steps"}), + "guidance_scale": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 20.0, "tooltip": "Guidance scale"}), + "output_format": (["png", "jpeg", "webp"], {"default": "png", "tooltip": "Output format"}), + }, + "optional": { + "reference_image_urls": ("STRING", {"multiline": True, "default": "", "tooltip": "Optional reference image URLs for subject-driven personalization, ONE PER LINE"}), + "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, + prompt: str, + image_size: str, + num_inference_steps: int, + guidance_scale: float, + output_format: str, + reference_image_urls: str = "", + poll_interval_sec: float = 2.0, + timeout_sec: int = 300, + ) -> Tuple[str, str]: + client = atlas_client.client + + p = (prompt or "").strip() + if not p: + raise RuntimeError("prompt is required") + + payload: Dict[str, Any] = { + "model": "hidream-o1-1.5/text-to-image", + "prompt": p, + "image_size": image_size, + "num_inference_steps": int(num_inference_steps), + "guidance_scale": float(guidance_scale), + "output_format": output_format, + } + + refs: List[str] = [u.strip() for u in (reference_image_urls or "").splitlines() if u.strip()] + if refs: + payload["reference_image_urls"] = refs + + prediction_id = client.generate_image(payload) + result = client.poll_prediction( + prediction_id, + poll_interval_sec=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) diff --git a/src/atlascloud_comfyui/nodes/video/sync_lipsync_v3.py b/src/atlascloud_comfyui/nodes/video/sync_lipsync_v3.py new file mode 100644 index 0000000..60a712c --- /dev/null +++ b/src/atlascloud_comfyui/nodes/video/sync_lipsync_v3.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import Any, Dict, Tuple + +from ..auth.atlas_client_node import AtlasClientHandle + + +class AtlasSyncLipsyncV3: + 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",), + "video": ("STRING", {"default": "", "tooltip": "Input video URL/base64 whose lips will be re-synced (mp4/mov/webm)"}), + "audio": ("STRING", {"default": "", "tooltip": "Driving audio URL/base64 to sync lips to (wav/mp3/m4a)"}), + "sync_mode": ( + ["cut_off", "loop", "bounce", "silence", "remap"], + {"default": "cut_off", "tooltip": "How to handle video/audio duration mismatch"}, + ), + }, + "optional": { + "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, + video: str, + audio: str, + sync_mode: str, + poll_interval_sec: float = 2.0, + timeout_sec: int = 900, + ) -> Tuple[str, str]: + video = (video or "").strip() + if not video: + raise RuntimeError("video is required (URL or base64)") + + audio = (audio or "").strip() + if not audio: + raise RuntimeError("audio is required (URL or base64)") + + client = atlas_client.client + + payload: Dict[str, Any] = { + "model": "sync/lipsync-v3", + "video_url": video, + "audio_url": audio, + "sync_mode": sync_mode, + } + + prediction_id = client.generate_video(payload) + result = client.poll_prediction(prediction_id, poll_interval_sec=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) diff --git a/src/atlascloud_comfyui/nodes/video/veed_lipsync.py b/src/atlascloud_comfyui/nodes/video/veed_lipsync.py new file mode 100644 index 0000000..5637899 --- /dev/null +++ b/src/atlascloud_comfyui/nodes/video/veed_lipsync.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Any, Dict, Tuple + +from ..auth.atlas_client_node import AtlasClientHandle + + +class AtlasVeedLipsync: + 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",), + "video": ("STRING", {"default": "", "tooltip": "Input talking-head video URL/base64 to re-lipsync (mp4/mov/webm)"}), + "audio": ("STRING", {"default": "", "tooltip": "Driving audio URL/base64 (wav/mp3/m4a)"}), + }, + "optional": { + "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, + video: str, + audio: str, + poll_interval_sec: float = 2.0, + timeout_sec: int = 900, + ) -> Tuple[str, str]: + video = (video or "").strip() + if not video: + raise RuntimeError("video is required (URL or base64)") + + audio = (audio or "").strip() + if not audio: + raise RuntimeError("audio is required (URL or base64)") + + client = atlas_client.client + + payload: Dict[str, Any] = { + "model": "veed/lipsync", + "video_url": video, + "audio_url": audio, + } + + prediction_id = client.generate_video(payload) + result = client.poll_prediction(prediction_id, poll_interval_sec=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) diff --git a/src/atlascloud_comfyui/registry.py b/src/atlascloud_comfyui/registry.py index d5cff01..e683252 100644 --- a/src/atlascloud_comfyui/registry.py +++ b/src/atlascloud_comfyui/registry.py @@ -157,6 +157,10 @@ from atlascloud_comfyui.nodes.image.nano_banana2_lite_t2i_dev import AtlasNanoBanana2LiteTextToImageDev from atlascloud_comfyui.nodes.image.nano_banana2_lite_edit import AtlasNanoBanana2LiteEdit from atlascloud_comfyui.nodes.image.nano_banana2_lite_edit_dev import AtlasNanoBanana2LiteEditDev +from atlascloud_comfyui.nodes.image.hidream_o1_15_t2i import AtlasHiDreamO115TextToImage +from atlascloud_comfyui.nodes.image.hidream_o1_15_edit import AtlasHiDreamO115Edit +from atlascloud_comfyui.nodes.video.sync_lipsync_v3 import AtlasSyncLipsyncV3 +from atlascloud_comfyui.nodes.video.veed_lipsync import AtlasVeedLipsync from atlascloud_comfyui.nodes.image.seedream_v50_lite_t2i import AtlasSeedreamV50LiteTextToImage from atlascloud_comfyui.nodes.image.seedream_v50_lite_edit import AtlasSeedreamV50LiteEdit from atlascloud_comfyui.nodes.image.seedream_v50_lite_sequential_t2i import AtlasSeedreamV50LiteSequentialTextToImage @@ -495,6 +499,10 @@ "AtlasCloud Nano Banana 2 Lite Text-to-Image Developer": AtlasNanoBanana2LiteTextToImageDev, "AtlasCloud Nano Banana 2 Lite Edit": AtlasNanoBanana2LiteEdit, "AtlasCloud Nano Banana 2 Lite Edit Developer": AtlasNanoBanana2LiteEditDev, + "AtlasCloud HiDream O1 1.5 Text-to-Image": AtlasHiDreamO115TextToImage, + "AtlasCloud HiDream O1 1.5 Edit": AtlasHiDreamO115Edit, + "AtlasCloud Sync Lipsync v3": AtlasSyncLipsyncV3, + "AtlasCloud VEED Lipsync": AtlasVeedLipsync, "AtlasCloud Seedream V5.0 Lite Text-to-Image": AtlasSeedreamV50LiteTextToImage, "AtlasCloud Seedream V5.0 Lite Sequential Text-to-Image": AtlasSeedreamV50LiteSequentialTextToImage, "AtlasCloud Seedream V5.0 Lite Edit": AtlasSeedreamV50LiteEdit, @@ -1020,6 +1028,10 @@ "AtlasCloud FLUX.2 Pro Edit": "AtlasCloud FLUX.2 Pro Edit", "AtlasCloud Grok Imagine Video Edit": "AtlasCloud Grok Imagine Video Edit", "AtlasCloud Grok Imagine Video Extend": "AtlasCloud Grok Imagine Video Extend", + "AtlasCloud HiDream O1 1.5 Text-to-Image": "AtlasCloud HiDream O1 1.5 Text-to-Image", + "AtlasCloud HiDream O1 1.5 Edit": "AtlasCloud HiDream O1 1.5 Edit", + "AtlasCloud Sync Lipsync v3": "AtlasCloud Sync Lipsync v3", + "AtlasCloud VEED Lipsync": "AtlasCloud VEED Lipsync", } diff --git a/tests/test_new_nodes_2026_07_03.py b/tests/test_new_nodes_2026_07_03.py new file mode 100644 index 0000000..75d7bc9 --- /dev/null +++ b/tests/test_new_nodes_2026_07_03.py @@ -0,0 +1,87 @@ +"""Metadata-only tests for newly added nodes (2026-07-03). + +HiDream O1 1.5 (text-to-image + edit) and lipsync nodes (sync/lipsync-v3, veed/lipsync). +These tests MUST NOT require ATLASCLOUD_API_KEY. +""" + + +def test_hidream_o1_15_t2i_metadata(): + from src.atlascloud_comfyui.nodes.image.hidream_o1_15_t2i import ( + AtlasHiDreamO115TextToImage, + ) + + required = AtlasHiDreamO115TextToImage.INPUT_TYPES()["required"] + assert "atlas_client" in required + assert "prompt" in required + assert AtlasHiDreamO115TextToImage.RETURN_TYPES == ("STRING", "STRING") + assert AtlasHiDreamO115TextToImage.CATEGORY == "AtlasCloud/Image" + + +def test_hidream_o1_15_edit_metadata(): + from src.atlascloud_comfyui.nodes.image.hidream_o1_15_edit import ( + AtlasHiDreamO115Edit, + ) + + required = AtlasHiDreamO115Edit.INPUT_TYPES()["required"] + assert "atlas_client" in required + assert "image" in required + assert "prompt" in required + assert AtlasHiDreamO115Edit.RETURN_TYPES == ("STRING", "STRING") + assert AtlasHiDreamO115Edit.CATEGORY == "AtlasCloud/Image" + + +def test_sync_lipsync_v3_metadata(): + from src.atlascloud_comfyui.nodes.video.sync_lipsync_v3 import AtlasSyncLipsyncV3 + + required = AtlasSyncLipsyncV3.INPUT_TYPES()["required"] + assert "atlas_client" in required + assert "video" in required + assert "audio" in required + assert "sync_mode" in required + assert AtlasSyncLipsyncV3.RETURN_TYPES == ("STRING", "STRING") + assert AtlasSyncLipsyncV3.CATEGORY == "AtlasCloud/Video" + + +def test_veed_lipsync_metadata(): + from src.atlascloud_comfyui.nodes.video.veed_lipsync import AtlasVeedLipsync + + required = AtlasVeedLipsync.INPUT_TYPES()["required"] + assert "atlas_client" in required + assert "video" in required + assert "audio" in required + assert AtlasVeedLipsync.RETURN_TYPES == ("STRING", "STRING") + assert AtlasVeedLipsync.CATEGORY == "AtlasCloud/Video" + + +def test_new_nodes_2026_07_03_registered(): + from src.atlascloud_comfyui.registry import ( + NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS, + ) + + for key in ( + "AtlasCloud HiDream O1 1.5 Text-to-Image", + "AtlasCloud HiDream O1 1.5 Edit", + "AtlasCloud Sync Lipsync v3", + "AtlasCloud VEED Lipsync", + ): + assert key in NODE_CLASS_MAPPINGS + assert key in NODE_DISPLAY_NAME_MAPPINGS + + +def test_new_nodes_2026_07_03_model_ids(): + import inspect + + from src.atlascloud_comfyui.nodes.image.hidream_o1_15_t2i import AtlasHiDreamO115TextToImage + from src.atlascloud_comfyui.nodes.image.hidream_o1_15_edit import AtlasHiDreamO115Edit + from src.atlascloud_comfyui.nodes.video.sync_lipsync_v3 import AtlasSyncLipsyncV3 + from src.atlascloud_comfyui.nodes.video.veed_lipsync import AtlasVeedLipsync + + expected = { + AtlasHiDreamO115TextToImage: "hidream-o1-1.5/text-to-image", + AtlasHiDreamO115Edit: "hidream-o1-1.5/edit", + AtlasSyncLipsyncV3: "sync/lipsync-v3", + AtlasVeedLipsync: "veed/lipsync", + } + for cls, model_id in expected.items(): + assert model_id in inspect.getsource(cls.run)