From 335dbc09aa5613a113a6b29dd18eca81112dcc93 Mon Sep 17 00:00:00 2001 From: alone Date: Wed, 18 Jun 2025 18:19:16 -0400 Subject: [PATCH] Initial release of Sync.so ComfyUI node --- __init__.py | 1 + requirements.txt | 2 + sync_node.py | 934 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 937 insertions(+) create mode 100644 __init__.py create mode 100644 requirements.txt create mode 100644 sync_node.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..7228647 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from .sync_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..46a67db --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +sync +requests diff --git a/sync_node.py b/sync_node.py new file mode 100644 index 0000000..a8a15c9 --- /dev/null +++ b/sync_node.py @@ -0,0 +1,934 @@ +# import time +# from pathlib import Path +# from sync import Sync +# from sync.common import Audio, Video, GenerationOptions +# from sync.core.api_error import ApiError + +# class SyncLipsyncNode: +# @classmethod +# def INPUT_TYPES(cls): +# return { +# "required": { +# "video_url": ("STRING", {"default": "https://assets.sync.so/docs/example-video.mp4"}), +# "audio_url": ("STRING", {"default": "https://assets.sync.so/docs/example-audio.wav"}), +# "api_key": ("STRING", {"default": ""}), +# "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), +# } +# } + +# RETURN_TYPES = ("STRING",) +# RETURN_NAMES = ("output_url",) +# FUNCTION = "lipsync_generate" +# CATEGORY = "Sync.so" +# OUTPUT_NODE = True # ✅ This makes it an output node + +# def lipsync_generate(self, video_url, audio_url, api_key, poll_interval): +# client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + +# try: +# response = client.create( +# input=[Video(url=video_url), Audio(url=audio_url)], +# model="lipsync-2", +# options=GenerationOptions(sync_mode="cut_off"), +# ) +# except ApiError as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Error creating generation: {e.status_code} - {e.body}",) +# } + +# job_id = response.id +# status = None +# while status not in ['COMPLETED', 'FAILED']: +# time.sleep(poll_interval) +# generation = client.get(job_id) +# status = generation.status + +# if status == 'COMPLETED': +# output_url = generation.output_url +# filename = f"sync_lipsync_output_{int(time.time())}.txt" +# output_dir = Path("output") +# output_dir.mkdir(exist_ok=True) +# output_path = output_dir / filename + +# with open(output_path, "w") as f: +# f.write(output_url) + +# return { +# "ui": { +# "texts": [{ +# "filename": filename, +# "subfolder": "", +# "type": "output" +# }] +# }, +# "result": (output_url,) +# } +# else: +# return { +# "ui": {"texts": []}, +# "result": (f"Generation failed for job {job_id}",) +# } + +# # Node registration for ComfyUI +# NODE_CLASS_MAPPINGS = { +# "SyncLipsyncNode": SyncLipsyncNode, +# } + +# NODE_DISPLAY_NAME_MAPPINGS = { +# "SyncLipsyncNode": "Sync.so Lipsync Generator" +# } + + + + +#----------------------------------------------------------------------------------------------- +#Save the video + +# import time +# import requests +# from pathlib import Path +# from sync import Sync +# from sync.common import Audio, Video, GenerationOptions +# from sync.core.api_error import ApiError + + +# class SyncLipsyncNode: +# @classmethod +# def INPUT_TYPES(cls): +# return { +# "required": { +# "video_url": ("STRING", {"default": "https://assets.sync.so/docs/example-video.mp4"}), +# "audio_url": ("STRING", {"default": "https://assets.sync.so/docs/example-audio.wav"}), +# "api_key": ("STRING", {"default": ""}), +# "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), +# } +# } + +# RETURN_TYPES = ("STRING",) +# RETURN_NAMES = ("output_path",) +# FUNCTION = "lipsync_generate" +# CATEGORY = "Sync.so" +# OUTPUT_NODE = True + +# def lipsync_generate(self, video_url, audio_url, api_key, poll_interval): +# client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + +# try: +# response = client.create( +# input=[Video(url=video_url), Audio(url=audio_url)], +# model="lipsync-2", +# options=GenerationOptions(sync_mode="cut_off"), +# ) +# except ApiError as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Error creating generation: {e.status_code} - {e.body}",) +# } + +# job_id = response.id +# status = None +# while status not in ['COMPLETED', 'FAILED']: +# time.sleep(poll_interval) +# generation = client.get(job_id) +# status = generation.status + +# if status == 'COMPLETED': +# output_url = generation.output_url +# timestamp = int(time.time()) +# output_dir = Path("output") +# output_dir.mkdir(exist_ok=True) +# output_filename = f"sync_output_{timestamp}.mp4" +# output_path = output_dir / output_filename + +# try: +# r = requests.get(output_url) +# r.raise_for_status() +# with open(output_path, "wb") as f: +# f.write(r.content) +# except Exception as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Failed to download video: {str(e)}",) +# } + +# return { +# "ui": { +# "videos": [{ +# "filename": output_filename, +# "subfolder": "", +# "type": "output" +# }] +# }, +# "result": (str(output_path),) +# } +# else: +# return { +# "ui": {"texts": []}, +# "result": (f"Generation failed for job {job_id}",) +# } + + +# # Node registration for ComfyUI +# NODE_CLASS_MAPPINGS = { +# "SyncLipsyncNode": SyncLipsyncNode, +# } + +# NODE_DISPLAY_NAME_MAPPINGS = { +# "SyncLipsyncNode": "Sync.so Lipsync Generator" +# } + +# print("✅ Sync.so node with video preview loaded.") + +# With multiple model support + +# import time +# import requests +# from pathlib import Path +# from sync import Sync +# from sync.common import Audio, Video, GenerationOptions +# from sync.core.api_error import ApiError + + +# class SyncLipsyncNode: +# @classmethod +# def INPUT_TYPES(cls): +# return { +# "required": { +# "video_url": ("STRING", {"default": "https://assets.sync.so/docs/example-video.mp4"}), +# "audio_url": ("STRING", {"default": "https://assets.sync.so/docs/example-audio.wav"}), +# "api_key": ("STRING", {"default": ""}), +# "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), +# "model": (["lipsync-2", "lipsync-1.9.0-beta"],), +# } +# } + +# RETURN_TYPES = ("STRING",) +# RETURN_NAMES = ("output_path",) +# FUNCTION = "lipsync_generate" +# CATEGORY = "Sync.so" +# OUTPUT_NODE = True + +# def lipsync_generate(self, video_url, audio_url, api_key, poll_interval, model): +# client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + +# try: +# response = client.create( +# input=[Video(url=video_url), Audio(url=audio_url)], +# model=model, +# options=GenerationOptions(sync_mode="cut_off"), +# ) +# except ApiError as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Error creating generation: {e.status_code} - {e.body}",) +# } + +# job_id = response.id +# status = None +# while status not in ['COMPLETED', 'FAILED']: +# time.sleep(poll_interval) +# generation = client.get(job_id) +# status = generation.status + +# if status == 'COMPLETED': +# output_url = generation.output_url +# timestamp = int(time.time()) +# output_dir = Path("output") +# output_dir.mkdir(exist_ok=True) +# output_filename = f"sync_output_{timestamp}.mp4" +# output_path = output_dir / output_filename + +# try: +# r = requests.get(output_url) +# r.raise_for_status() +# with open(output_path, "wb") as f: +# f.write(r.content) +# except Exception as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Failed to download video: {str(e)}",) +# } + +# return { +# "ui": { +# "videos": [{ +# "filename": output_filename, +# "subfolder": "", +# "type": "output" +# }] +# }, +# "result": (str(output_path),) +# } +# else: +# return { +# "ui": {"texts": []}, +# "result": (f"Generation failed for job {job_id}",) +# } + + +# # Node registration for ComfyUI +# NODE_CLASS_MAPPINGS = { +# "SyncLipsyncNode": SyncLipsyncNode, +# } + +# NODE_DISPLAY_NAME_MAPPINGS = { +# "SyncLipsyncNode": "Sync.so Lipsync Generator" +# } + +# print("✅ Sync.so node with video preview loaded.") + + + + +# import time +# import requests +# from pathlib import Path +# from sync import Sync +# from sync.common import Audio, Video, GenerationOptions +# from sync.core.api_error import ApiError + + +# class SyncLipsyncNode: +# @classmethod +# def INPUT_TYPES(cls): +# return { +# "required": { +# "video_url": ("STRING", {"default": "https://assets.sync.so/docs/example-video.mp4"}), +# "audio_url": ("STRING", {"default": "https://assets.sync.so/docs/example-audio.wav"}), +# "api_key": ("STRING", {"default": ""}), +# "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), +# "model": (["lipsync-2", "lipsync-1.9.0-beta"],), +# "segment_secs": ("STRING", {"default": ""}), +# "segment_frames": ("STRING", {"default": ""}), +# } +# } + +# RETURN_TYPES = ("STRING",) +# RETURN_NAMES = ("output_path",) +# FUNCTION = "lipsync_generate" +# CATEGORY = "Sync.so" +# OUTPUT_NODE = True + +# def lipsync_generate(self, video_url, audio_url, api_key, poll_interval, model, segment_secs, segment_frames): +# client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + +# # Prepare video kwargs +# video_kwargs = {} +# if segment_secs: +# try: +# video_kwargs["segments_secs"] = eval(segment_secs) +# except Exception: +# return { +# "ui": {"texts": []}, +# "result": ("Invalid format for segment_secs. Example: [[5.0, 10.0]]",) +# } + +# if segment_frames: +# try: +# video_kwargs["segments_frames"] = eval(segment_frames) +# except Exception: +# return { +# "ui": {"texts": []}, +# "result": ("Invalid format for segment_frames. Example: [[100, 200]]",) +# } + +# try: +# response = client.create( +# input=[Video(url=video_url, **video_kwargs), Audio(url=audio_url)], +# model=model, +# options=GenerationOptions(sync_mode="cut_off"), +# ) +# except ApiError as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Error creating generation: {e.status_code} - {e.body}",) +# } + +# job_id = response.id +# status = None +# while status not in ['COMPLETED', 'FAILED']: +# time.sleep(poll_interval) +# generation = client.get(job_id) +# status = generation.status + +# if status == 'COMPLETED': +# output_url = generation.output_url +# timestamp = int(time.time()) +# output_dir = Path("output") +# output_dir.mkdir(exist_ok=True) +# output_filename = f"sync_output_{timestamp}.mp4" +# output_path = output_dir / output_filename + +# try: +# r = requests.get(output_url) +# r.raise_for_status() +# with open(output_path, "wb") as f: +# f.write(r.content) +# except Exception as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Failed to download video: {str(e)}",) +# } + +# return { +# "ui": { +# "videos": [{ +# "filename": output_filename, +# "subfolder": "", +# "type": "output" +# }] +# }, +# "result": (str(output_path),) +# } +# else: +# return { +# "ui": {"texts": []}, +# "result": (f"Generation failed for job {job_id}",) +# } + + +# # Node registration for ComfyUI +# NODE_CLASS_MAPPINGS = { +# "SyncLipsyncNode": SyncLipsyncNode, +# } + +# NODE_DISPLAY_NAME_MAPPINGS = { +# "SyncLipsyncNode": "Sync.so Lipsync Generator" +# } + +# print("✅ Sync.so node with video preview loaded.") + + + + +# IS this the END!!!! Filhaal ye acha hai + + + +import time +import requests +from pathlib import Path +from sync import Sync +from sync.common import Audio, Video, GenerationOptions +from sync.core.api_error import ApiError + + +class SyncLipsyncNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "video_url": ("STRING", {"default": "https://assets.sync.so/docs/example-video.mp4"}), + "audio_url": ("STRING", {"default": "https://assets.sync.so/docs/example-audio.wav"}), + "api_key": ("STRING", {"default": ""}), + "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), + "model": (["lipsync-2", "lipsync-1.9.0-beta"],), + "segment_secs": ("STRING", {"default": ""}), + "segment_frames": ("STRING", {"default": ""}), + "sync_mode": (["loop", "bounce", "cut_off", "silence", "remap"], {"default": "cut_off"}), + "temperature": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0}), + "active_speaker": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("output_path",) + FUNCTION = "lipsync_generate" + CATEGORY = "Sync.so" + OUTPUT_NODE = True + + def lipsync_generate( + self, + video_url, + audio_url, + api_key, + poll_interval, + model, + segment_secs, + segment_frames, + sync_mode, + temperature, + active_speaker, + ): + client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + + # Prepare video kwargs + video_kwargs = {} + if segment_secs: + try: + video_kwargs["segments_secs"] = eval(segment_secs) + except Exception: + return { + "ui": {"texts": []}, + "result": ("Invalid format for segment_secs. Example: [[5.0, 10.0]]",) + } + + if segment_frames: + try: + video_kwargs["segments_frames"] = eval(segment_frames) + except Exception: + return { + "ui": {"texts": []}, + "result": ("Invalid format for segment_frames. Example: [[100, 200]]",) + } + + try: + response = client.create( + input=[Video(url=video_url, **video_kwargs), Audio(url=audio_url)], + model=model, + options=GenerationOptions( + sync_mode=sync_mode, + temperature=temperature, + active_speaker=active_speaker, + ), + ) + except ApiError as e: + return { + "ui": {"texts": []}, + "result": (f"Error creating generation: {e.status_code} - {e.body}",) + } + + job_id = response.id + status = None + while status not in ['COMPLETED', 'FAILED']: + time.sleep(poll_interval) + generation = client.get(job_id) + status = generation.status + + if status == 'COMPLETED': + output_url = generation.output_url + timestamp = int(time.time()) + output_dir = Path("output") + output_dir.mkdir(exist_ok=True) + output_filename = f"sync_output_{timestamp}.mp4" + output_path = output_dir / output_filename + + try: + r = requests.get(output_url) + r.raise_for_status() + with open(output_path, "wb") as f: + f.write(r.content) + except Exception as e: + return { + "ui": {"texts": []}, + "result": (f"Failed to download video: {str(e)}",) + } + + return { + "ui": { + "videos": [{ + "filename": output_filename, + "subfolder": "", + "type": "output" + }] + }, + "result": (str(output_path),) + } + else: + return { + "ui": {"texts": []}, + "result": (f"Generation failed for job {job_id}",) + } + + +# Node registration for ComfyUI +NODE_CLASS_MAPPINGS = { + "SyncLipsyncNode": SyncLipsyncNode, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SyncLipsyncNode": "Sync.so Lipsync Generator" +} + +print("✅ Sync.so node with video preview loaded.") + +###ENDDDDDD + +# import time +# import json +# import requests +# from pathlib import Path +# from sync import Sync +# from sync.common import Audio, Video, GenerationOptions +# from sync.core.api_error import ApiError + + +# class SyncLipsyncNode: +# @classmethod +# def INPUT_TYPES(cls): +# return { +# "required": { +# "video_url": ("STRING", {"default": "https://assets.sync.so/docs/example-video.mp4"}), +# "audio_url": ("STRING", {"default": ""}), # optional if TTS used +# "tts_text": ("STRING", {"default": ""}), # text for TTS +# "tts_provider": ("STRING", {"default": ""}), # JSON string for TTS provider config +# "api_key": ("STRING", {"default": ""}), +# "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), +# "model": (["lipsync-2", "lipsync-1.9.0-beta"],), +# "segment_secs": ("STRING", {"default": ""}), +# "segment_frames": ("STRING", {"default": ""}), +# "sync_mode": (["loop", "bounce", "cut_off", "silence", "remap"], {"default": "cut_off"}), +# "temperature": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0}), +# "active_speaker": ("BOOLEAN", {"default": False}), +# } +# } + +# RETURN_TYPES = ("STRING",) +# RETURN_NAMES = ("output_path",) +# FUNCTION = "lipsync_generate" +# CATEGORY = "Sync.so" +# OUTPUT_NODE = True + +# def lipsync_generate( +# self, +# video_url, +# audio_url, +# tts_text, +# tts_provider, +# api_key, +# poll_interval, +# model, +# segment_secs, +# segment_frames, +# sync_mode, +# temperature, +# active_speaker, +# ): +# client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + +# # Prepare video kwargs +# video_kwargs = {} +# if segment_secs: +# try: +# video_kwargs["segments_secs"] = eval(segment_secs) +# except Exception: +# return { +# "ui": {"texts": []}, +# "result": ("Invalid format for segment_secs. Example: [[5.0, 10.0]]",) +# } + +# if segment_frames: +# try: +# video_kwargs["segments_frames"] = eval(segment_frames) +# except Exception: +# return { +# "ui": {"texts": []}, +# "result": ("Invalid format for segment_frames. Example: [[100, 200]]",) +# } + +# # Prepare inputs: video + (audio or TTS) +# inputs = [Video(url=video_url, **video_kwargs)] + +# if tts_text.strip(): +# # Use TTS input +# try: +# provider_config = json.loads(tts_provider) if tts_provider else {} +# except Exception as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Invalid JSON format for tts_provider: {str(e)}",) +# } +# inputs.append( +# { +# "type": "text", +# "provider": provider_config +# } +# ) +# # Note: Sync SDK might require a special class or raw dict. Using dict as per spec. +# elif audio_url.strip(): +# # Use audio URL input +# inputs.append(Audio(url=audio_url)) +# else: +# return { +# "ui": {"texts": []}, +# "result": ("Either audio_url or tts_text with tts_provider must be provided.",) +# } + +# try: +# response = client.create( +# input=inputs, +# model=model, +# options=GenerationOptions( +# sync_mode=sync_mode, +# temperature=temperature, +# active_speaker=active_speaker, +# ), +# ) +# except ApiError as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Error creating generation: {e.status_code} - {e.body}",) +# } + +# job_id = response.id +# status = None +# while status not in ['COMPLETED', 'FAILED']: +# time.sleep(poll_interval) +# generation = client.get(job_id) +# status = generation.status + +# if status == 'COMPLETED': +# output_url = generation.output_url +# timestamp = int(time.time()) +# output_dir = Path("output") +# output_dir.mkdir(exist_ok=True) +# output_filename = f"sync_output_{timestamp}.mp4" +# output_path = output_dir / output_filename + +# try: +# r = requests.get(output_url) +# r.raise_for_status() +# with open(output_path, "wb") as f: +# f.write(r.content) +# except Exception as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Failed to download video: {str(e)}",) +# } + +# return { +# "ui": { +# "videos": [{ +# "filename": output_filename, +# "subfolder": "", +# "type": "output" +# }] +# }, +# "result": (str(output_path),) +# } +# else: +# return { +# "ui": {"texts": []}, +# "result": (f"Generation failed for job {job_id}",) +# } + + +# # Node registration for ComfyUI +# NODE_CLASS_MAPPINGS = { +# "SyncLipsyncNode": SyncLipsyncNode, +# } + +# NODE_DISPLAY_NAME_MAPPINGS = { +# "SyncLipsyncNode": "Sync.so Lipsync Generator" +# } + +# print("✅ Sync.so node with video preview loaded.") + + + + + + + + + + + + + + + + + +#--------------------------------------------------------------------------------------------------------------------------- + +# import time +# from pathlib import Path +# from sync import Sync +# from sync.common import Audio, Video, GenerationOptions +# from sync.core.api_error import ApiError + +# class SyncLipsyncNode: +# @classmethod +# def INPUT_TYPES(cls): +# return { +# "required": { +# "video": ("VIDEO",), +# "audio": ("AUDIO",), +# "api_key": ("STRING", {"default": ""}), +# "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), +# } +# } + +# RETURN_TYPES = ("STRING",) +# RETURN_NAMES = ("output_url",) +# FUNCTION = "lipsync_generate" +# CATEGORY = "Sync.so" +# OUTPUT_NODE = True + +# def lipsync_generate(self, video, audio, api_key, poll_interval): +# # Extract file paths from VIDEO/AUDIO objects +# # video_path = Path(video["filepath"]).resolve().as_uri() +# # audio_path = Path(audio["filepath"]).resolve().as_uri() +# video_path = Path(video["path"]).resolve().as_uri() +# audio_path = Path(audio["path"]).resolve().as_uri() + + + +# client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + +# try: +# response = client.create( +# input=[Video(url=video_path), Audio(url=audio_path)], +# model="lipsync-2", +# options=GenerationOptions(sync_mode="cut_off"), +# ) +# except ApiError as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Error creating generation: {e.status_code} - {e.body}",) +# } + +# job_id = response.id +# status = None +# while status not in ['COMPLETED', 'FAILED']: +# time.sleep(poll_interval) +# generation = client.get(job_id) +# status = generation.status + +# if status == 'COMPLETED': +# output_url = generation.output_url +# filename = f"sync_lipsync_output_{int(time.time())}.txt" +# output_dir = Path("output") +# output_dir.mkdir(exist_ok=True) +# output_path = output_dir / filename + +# with open(output_path, "w") as f: +# f.write(output_url) + +# return { +# "ui": { +# "texts": [{ +# "filename": filename, +# "subfolder": "", +# "type": "output" +# }] +# }, +# "result": (output_url,) +# } +# else: +# return { +# "ui": {"texts": []}, +# "result": (f"Generation failed for job {job_id}",) +# } + +# # Register the node +# NODE_CLASS_MAPPINGS = { +# "SyncLipsyncNode": SyncLipsyncNode, +# } + +# NODE_DISPLAY_NAME_MAPPINGS = { +# "SyncLipsyncNode": "Sync.so Lipsync Generator" +# } +# import time +# from pathlib import Path +# from sync import Sync +# from sync.common import Audio, Video, GenerationOptions +# from sync.core.api_error import ApiError + +# import soundfile as sf +# import torch +# import tempfile + +# class SyncLipsyncNode: +# @classmethod +# def INPUT_TYPES(cls): +# return { +# "required": { +# "video": ("VIDEO",), +# "audio": ("AUDIO",), +# "api_key": ("STRING", {"default": ""}), +# "poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}), +# } +# } + +# RETURN_TYPES = ("STRING",) +# RETURN_NAMES = ("output_url",) +# FUNCTION = "lipsync_generate" +# CATEGORY = "Sync.so" +# OUTPUT_NODE = True + +# def save_audio_to_file(self, audio_dict): +# waveform = audio_dict["waveform"] +# sample_rate = audio_dict["sample_rate"] + +# # Convert tensor to numpy array with shape (samples, channels) +# if isinstance(waveform, torch.Tensor): +# waveform = waveform.squeeze().permute(1, 0).cpu().numpy() +# else: +# waveform = waveform.T # Just in case it's numpy already + +# # Save to temporary WAV file +# tmp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) +# sf.write(tmp_file.name, waveform, sample_rate) +# tmp_file.close() +# return tmp_file.name + +# def lipsync_generate(self, video, audio, api_key, poll_interval): +# # Extract video path from VideoFromFile object (private attribute) +# video_path_str = getattr(video, "_VideoFromFile__file", None) +# if not video_path_str: +# raise RuntimeError("Video path not found") + +# video_path = Path(video_path_str).resolve().as_uri() + +# # Save audio waveform dict to WAV file and get path +# audio_file_path = self.save_audio_to_file(audio) +# audio_path = Path(audio_file_path).resolve().as_uri() + +# client = Sync(base_url="https://api.sync.so", api_key=api_key).generations + +# try: +# response = client.create( +# input=[Video(url=video_path), Audio(url=audio_path)], +# model="lipsync-2", +# options=GenerationOptions(sync_mode="cut_off"), +# ) +# except ApiError as e: +# return { +# "ui": {"texts": []}, +# "result": (f"Error creating generation: {e.status_code} - {e.body}",) +# } + +# job_id = response.id +# status = None +# while status not in ['COMPLETED', 'FAILED']: +# time.sleep(poll_interval) +# generation = client.get(job_id) +# status = generation.status + +# if status == 'COMPLETED': +# output_url = generation.output_url +# filename = f"sync_lipsync_output_{int(time.time())}.txt" +# output_dir = Path("output") +# output_dir.mkdir(exist_ok=True) +# output_path = output_dir / filename + +# with open(output_path, "w") as f: +# f.write(output_url) + +# return { +# "ui": { +# "texts": [{ +# "filename": filename, +# "subfolder": "", +# "type": "output" +# }] +# }, +# "result": (output_url,) +# } +# else: +# return { +# "ui": {"texts": []}, +# "result": (f"Generation failed for job {job_id}",) +# } + +# # Register the node +# NODE_CLASS_MAPPINGS = { +# "SyncLipsyncNode": SyncLipsyncNode, +# } + +# NODE_DISPLAY_NAME_MAPPINGS = { +# "SyncLipsyncNode": "Sync.so Lipsync Generator" +# }