Update sync_node.py with latest changes
This commit is contained in:
+310
-99
@@ -1,20 +1,185 @@
|
||||
import time
|
||||
import requests
|
||||
# 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": ""}),
|
||||
# "audio_url": ("STRING", {"default": ""}),
|
||||
# "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.")
|
||||
|
||||
import time, requests
|
||||
from pathlib import Path
|
||||
from os.path import getsize
|
||||
from sync import Sync
|
||||
from sync.common import Audio, Video, GenerationOptions
|
||||
from sync.core.api_error import ApiError
|
||||
|
||||
|
||||
class SyncLipsyncNode:
|
||||
# ────────── INPUT NODE
|
||||
class SyncLipsyncInputNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"video_url": ("STRING", {"default": ""}),
|
||||
"audio_url": ("STRING", {"default": ""}),
|
||||
"api_key": ("STRING", {"default": ""}),
|
||||
"video_path": ("STRING", {"default": ""}),
|
||||
"audio_path": ("STRING", {"default": ""}),
|
||||
"video_url": ("STRING", {"default": ""}),
|
||||
"audio_url": ("STRING", {"default": ""}),
|
||||
"api_key": ("STRING", {"default": ""}),
|
||||
"poll_interval": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("SYNC_INPUT",)
|
||||
RETURN_NAMES = ("sync_input",)
|
||||
FUNCTION = "provide"
|
||||
CATEGORY = "Sync.so/Lipsync"
|
||||
|
||||
def provide(self, video_path, audio_path, video_url, audio_url, api_key, poll_interval):
|
||||
return ({
|
||||
"video_path": video_path,
|
||||
"audio_path": audio_path,
|
||||
"video_url": video_url,
|
||||
"audio_url": audio_url,
|
||||
"api_key": api_key,
|
||||
"poll_interval": poll_interval,
|
||||
},)
|
||||
|
||||
|
||||
# ────────── GENERATE NODE
|
||||
class SyncLipsyncMainNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"sync_input": ("SYNC_INPUT", {}),
|
||||
"model": (["lipsync-2", "lipsync-1.9.0-beta"],),
|
||||
"segment_secs": ("STRING", {"default": ""}),
|
||||
"segment_frames": ("STRING", {"default": ""}),
|
||||
@@ -27,110 +192,156 @@ class SyncLipsyncNode:
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("output_path",)
|
||||
FUNCTION = "lipsync_generate"
|
||||
CATEGORY = "Sync.so"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "Sync.so/Lipsync"
|
||||
|
||||
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
|
||||
def lipsync_generate(self, sync_input, model, segment_secs, segment_frames, sync_mode, temperature, active_speaker):
|
||||
video_path = sync_input["video_path"]
|
||||
audio_path = sync_input["audio_path"]
|
||||
video_url = sync_input["video_url"]
|
||||
audio_url = sync_input["audio_url"]
|
||||
api_key = sync_input["api_key"]
|
||||
poll_interval = sync_input["poll_interval"]
|
||||
|
||||
# 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]]",)
|
||||
}
|
||||
print(" lipsync_generate called")
|
||||
|
||||
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]]",)
|
||||
}
|
||||
MAX_BYTES = 20 * 1024 * 1024
|
||||
headers = {"x-api-key": api_key}
|
||||
|
||||
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)}",)
|
||||
if (video_path and Path(video_path).exists() and getsize(video_path) <= MAX_BYTES) or \
|
||||
(audio_path and Path(audio_path).exists() and getsize(audio_path) <= MAX_BYTES):
|
||||
print(" Using file upload (v2)")
|
||||
url = "https://api.sync.so/v2/generate"
|
||||
data = {
|
||||
"model": model,
|
||||
"sync_mode": sync_mode,
|
||||
"temperature": str(temperature),
|
||||
"active_speaker": str(active_speaker).lower(),
|
||||
}
|
||||
|
||||
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}",)
|
||||
}
|
||||
if segment_secs:
|
||||
data["segments_secs"] = segment_secs
|
||||
if segment_frames:
|
||||
data["segments_frames"] = segment_frames
|
||||
|
||||
files = {}
|
||||
if video_path and Path(video_path).exists():
|
||||
files["video"] = open(video_path, "rb")
|
||||
print(f" Opening video file: {video_path}")
|
||||
elif video_url:
|
||||
data["video_url"] = video_url
|
||||
print(f" Using video URL: {video_url}")
|
||||
|
||||
if audio_path and Path(audio_path).exists():
|
||||
files["audio"] = open(audio_path, "rb")
|
||||
print(f" Opening audio file: {audio_path}")
|
||||
elif audio_url:
|
||||
data["audio_url"] = audio_url
|
||||
print(f" Using audio URL: {audio_url}")
|
||||
|
||||
print(" Sending POST request...")
|
||||
res = requests.post(url, headers=headers, files=files or None, data=data)
|
||||
print(f" Response code: {res.status_code}")
|
||||
res.raise_for_status()
|
||||
job_id = res.json()["id"]
|
||||
print(f" Job ID: {job_id}")
|
||||
else:
|
||||
print(" Using SDK fallback")
|
||||
client = Sync(base_url="https://api.sync.so", api_key=api_key).generations
|
||||
video_kwargs = {}
|
||||
if segment_secs:
|
||||
video_kwargs["segments_secs"] = eval(segment_secs)
|
||||
if segment_frames:
|
||||
video_kwargs["segments_frames"] = eval(segment_frames)
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
job_id = response.id
|
||||
|
||||
# Polling
|
||||
status_url = f"https://api.sync.so/v2/generate/{job_id}"
|
||||
print(f" Polling job: {job_id}")
|
||||
status = None
|
||||
|
||||
while status not in ["COMPLETED", "FAILED"]:
|
||||
print(f" Waiting {poll_interval}s...")
|
||||
time.sleep(poll_interval)
|
||||
poll = requests.get(status_url, headers=headers)
|
||||
poll.raise_for_status()
|
||||
status = poll.json().get("status")
|
||||
print(f" Job status: {status}")
|
||||
|
||||
if status != "COMPLETED":
|
||||
print(f" Job failed")
|
||||
return ("",)
|
||||
|
||||
output_url = poll.json().get("outputUrl") or (poll.json().get("result") or {}).get("outputUrl")
|
||||
print(f" Downloading video from: {output_url}")
|
||||
timestamp = int(time.time())
|
||||
output_path = Path("output") / f"sync_output_{timestamp}.mp4"
|
||||
Path("output").mkdir(exist_ok=True)
|
||||
|
||||
r = requests.get(output_url)
|
||||
r.raise_for_status()
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(r.content)
|
||||
print(f" Video saved to: {output_path}")
|
||||
return (str(output_path),)
|
||||
|
||||
except Exception as e:
|
||||
print(f"🔥 Exception: {e}")
|
||||
return ("",)
|
||||
|
||||
|
||||
# Node registration for ComfyUI
|
||||
# ────────── OUTPUT NODE
|
||||
class SyncLipsyncOutputNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"output_path": ("STRING", {})}}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("output_path",)
|
||||
FUNCTION = "passthrough"
|
||||
CATEGORY = "Sync.so/Lipsync"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def passthrough(self, output_path):
|
||||
if not output_path or not Path(output_path).exists():
|
||||
return {
|
||||
"ui": {"texts": [" No video output was generated."]},
|
||||
"result": (output_path,)
|
||||
}
|
||||
|
||||
return {
|
||||
"ui": {
|
||||
"videos": [{
|
||||
"filename": Path(output_path).name,
|
||||
"subfolder": "",
|
||||
"type": "output"
|
||||
}]
|
||||
},
|
||||
"result": (output_path,)
|
||||
}
|
||||
|
||||
|
||||
# ────────── REGISTER
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SyncLipsyncNode": SyncLipsyncNode,
|
||||
"SyncLipsyncInputNode": SyncLipsyncInputNode,
|
||||
"SyncLipsyncMainNode": SyncLipsyncMainNode,
|
||||
"SyncLipsyncOutputNode": SyncLipsyncOutputNode,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SyncLipsyncNode": "Sync.so Lipsync Generator"
|
||||
"SyncLipsyncInputNode": "Sync.so Lipsync – Input",
|
||||
"SyncLipsyncMainNode": "Sync.so Lipsync – Generate",
|
||||
"SyncLipsyncOutputNode": "Sync.so Lipsync – Output",
|
||||
}
|
||||
|
||||
print("✅ Sync.so node with video preview loaded.")
|
||||
print("Sync.so hybrid upload module loaded.")
|
||||
|
||||
Reference in New Issue
Block a user