dennny123's picture
Clean up logs and install performance optimizations
d014e57
import os
import sys
import subprocess
import time
import requests
import json
import gradio as gr
from PIL import Image
import spaces
from huggingface_hub import hf_hub_download
os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
# Suppress unnecessary warnings
os.environ["PYTHONWARNINGS"] = "ignore"
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
# Configuration
REPO_URL = "https://github.com/00quebec/Synthid-Bypass"
COMFYUI_URL = "https://github.com/comfyanonymous/ComfyUI"
PYTHON_EXTENSION_URL = "https://github.com/pydn/ComfyUI-to-Python-Extension"
ROOT_DIR = os.getcwd()
COMFYUI_DIR = os.path.join(ROOT_DIR, "ComfyUI")
BYPASS_REPO_DIR = os.path.join(ROOT_DIR, "reference_repo")
def setup():
"""Environment setup for Hugging Face Space"""
# Check if a known model file exists to skip setup
if os.path.exists(os.path.join(COMFYUI_DIR, "models/vae/ae.safetensors")):
print("--- ENVIRONMENT ALREADY INITIALIZED ---")
return
print("--- FIRST TIME SETUP STARTING ---")
# 1. Clone Repos
subprocess.run(["git", "clone", COMFYUI_URL, COMFYUI_DIR], check=True, capture_output=True)
subprocess.run(["git", "clone", REPO_URL, BYPASS_REPO_DIR], check=True, capture_output=True)
# 2. Setup Custom Nodes
nodes = [
"https://github.com/ltdrdata/ComfyUI-Impact-Pack",
"https://github.com/ltdrdata/ComfyUI-Impact-Subpack",
"https://github.com/wildminder/ComfyUI-dype",
"https://github.com/rgthree/rgthree-comfy",
"https://github.com/BadCafeCode/masquerade-nodes-comfyui",
"https://github.com/lquesada/ComfyUI-Inpaint-CropAndStitch",
"https://github.com/numz/ComfyUI-SeedVR2_VideoUpscaler",
PYTHON_EXTENSION_URL
]
custom_nodes_path = os.path.join(COMFYUI_DIR, "custom_nodes")
os.makedirs(custom_nodes_path, exist_ok=True)
# Pin Impact packs to exact versions used in reference workflow
IMPACT_PACK_COMMIT = "61bd8397a18e7e7668e6a24e95168967768c2bed"
IMPACT_SUBPACK_VERSION = "1.3.4" # Using 1.3.4 (latest available, ref workflow uses "1.3.5" which doesn't exist as tag)
for url in nodes:
name = url.split("/")[-1]
node_dest = os.path.join(custom_nodes_path, name)
if not os.path.exists(node_dest):
subprocess.run(["git", "clone", url, node_dest], check=True, capture_output=True)
# Checkout specific versions for Impact packs to match reference workflow
if name == "ComfyUI-Impact-Pack":
subprocess.run(["git", "checkout", IMPACT_PACK_COMMIT], cwd=node_dest, check=True, capture_output=True)
elif name == "ComfyUI-Impact-Subpack":
subprocess.run(["git", "checkout", IMPACT_SUBPACK_VERSION], cwd=node_dest, check=True, capture_output=True)
print("✓ Custom nodes installed")
# Install performance optimizations (SageAttention, Flash Attention)
print("Installing performance optimizations...")
subprocess.run([
sys.executable, "-m", "pip", "install",
"sageattention", "flash-attn", "--no-cache-dir"
], capture_output=True, check=False) # Don't fail if these can't install
# 3. Models Download logic (Using hf_transfer for speed)
model_configs = [
{"repo": "Comfy-Org/z_image_turbo", "file": "split_files/vae/ae.safetensors", "dest": "models/vae/ae.safetensors"},
{"repo": "Comfy-Org/z_image_turbo", "file": "split_files/diffusion_models/z_image_turbo_bf16.safetensors", "dest": "models/diffusion_models/z_image_turbo_bf16.safetensors"},
{"repo": "Comfy-Org/z_image_turbo", "file": "split_files/text_encoders/qwen_3_4b.safetensors", "dest": "models/text_encoders/qwen_3_4b.safetensors"},
{"repo": "alibaba-pai/Z-Image-Turbo-Fun-Controlnet-Union", "file": "Z-Image-Turbo-Fun-Controlnet-Union.safetensors", "dest": "models/model_patches/Z-Image-Turbo-Fun-Controlnet-Union.safetensors"},
{"repo": "deepghs/yolo-face", "file": "yolov8n-face/model.pt", "dest": "models/ultralytics/bbox/yolov8n-face.pt"},
{"repo": "YouLiXiya/YL-SAM", "file": "sam_vit_b_01ec64.pth", "dest": "models/sams/sam_vit_b_01ec64.pth"},
# SeedVR2 models (15.3GB DiT + 672MB VAE)
{"repo": "numz/SeedVR2_comfyUI", "file": "seedvr2_ema_7b_sharp_fp16.safetensors", "dest": "models/SEEDVR2/seedvr2_ema_7b_sharp_fp16.safetensors"},
{"repo": "numz/SeedVR2_comfyUI", "file": "ema_vae_fp16.safetensors", "dest": "models/SEEDVR2/ema_vae_fp16.safetensors"}
]
print("Downloading models (fast with HF_TRANSFER)...")
for i, cfg in enumerate(model_configs, 1):
out_path = os.path.join(COMFYUI_DIR, cfg['dest'])
if not os.path.exists(out_path):
os.makedirs(os.path.dirname(out_path), exist_ok=True)
print(f" [{i}/{len(model_configs)}] {cfg['file'].split('/')[-1]}")
hf_hub_download(
repo_id=cfg['repo'],
filename=cfg['file'],
local_dir=COMFYUI_DIR,
local_dir_use_symlinks=False
)
actual_downloaded_path = os.path.join(COMFYUI_DIR, cfg['file'])
if actual_downloaded_path != out_path and os.path.exists(actual_downloaded_path):
os.rename(actual_downloaded_path, out_path)
print("✓ Setup complete")
def convert_to_api(web_workflow):
"""
Robustly converts ComfyUI Web JSON (UI format) to API Prompt format.
Requires mapping links to actual node connections.
"""
nodes = web_workflow.get("nodes", [])
links = web_workflow.get("links", [])
# Map link_id -> [origin_node_id, origin_slot_index]
link_map = {}
for link in links:
if link:
l_id, node_from, slot_from, node_to, slot_to, l_type = link
link_map[l_id] = [str(node_from), slot_from]
api_prompt = {}
skipped_nodes = []
for node in nodes:
node_id = str(node["id"])
class_type = node["type"]
# Skip UI-only nodes, provider/loader nodes, and primitive types
skip_types = ["Note", "Group", "Reroute", "Float", "Int", "String", "Boolean"]
# Also skip any node with "Provider" or "Loader" in the name (these are config nodes)
if class_type in skip_types or "Provider" in class_type or "Loader" in class_type and class_type not in ["UNETLoader", "VAELoader", "CLIPLoader"]:
skipped_nodes.append(f"{node_id}:{class_type}")
continue
inputs = {}
# 1. Handle Connections (from links)
for inp in node.get("inputs", []):
l_id = inp.get("link")
if l_id and l_id in link_map:
inputs[inp["name"]] = link_map[l_id]
# 2. Handle Widgets (from widgets_values)
# This is where it gets tricky since Web format stores values in a list
# and API format expects them as named keys.
# We'll use a known mapping for core nodes if possible.
# For custom nodes, it depends on the node's implementation of 'INPUT_TYPES'.
# Note: If the workflow was saved with 'widgets_values', we inject them.
# We'll try to guess common input names or just pass them as indices if the server allows.
# For SynthID-Bypass, we'll hardcode the critical ones if needed.
# Fallback: Many nodes put widgets after connections in their registration.
# If we don't have names, it might fail.
# However, many modern workflows save 'widgets_values' which we need to map.
# For this specific bypass tool, we'll use the pre-known node names for key nodes.
w_values = node.get("widgets_values", [])
if class_type == "CLIPTextEncode" and w_values:
inputs["text"] = w_values[0]
elif class_type == "KSampler" and len(w_values) >= 7:
inputs["seed"] = w_values[0]
inputs["steps"] = w_values[2]
inputs["cfg"] = w_values[3]
inputs["sampler_name"] = w_values[4]
inputs["scheduler"] = w_values[5]
inputs["denoise"] = w_values[6]
elif class_type == "VAELoader" and w_values:
inputs["vae_name"] = w_values[0]
elif class_type == "UNETLoader" and w_values:
inputs["unet_name"] = w_values[0]
elif class_type == "LoadImage" and w_values:
inputs["image"] = w_values[0]
inputs["upload"] = w_values[1] if len(w_values) > 1 else "image"
elif class_type == "ModelSamplingAuraFlow" and w_values:
inputs["shift"] = w_values[0]
elif class_type == "DyPE_FLUX" and len(w_values) >= 4:
inputs["width"] = w_values[0]
inputs["height"] = w_values[1]
inputs["preset"] = w_values[2]
inputs["pe_type"] = w_values[3]
# Advanced Peet's parameters... usually defaults are okay but we can add more if needed
# Add any other widget values that might be present
# This is a guestimate, but is usually how API conversion works
api_prompt[node_id] = {
"class_type": class_type,
"inputs": inputs
}
print(f"Converted {len(api_prompt)} nodes, skipped {len(skipped_nodes)} nodes: {', '.join(skipped_nodes[:10])}")
return api_prompt
# Execute setup on boot
setup()
@spaces.GPU(duration=120)
def remove_watermark(input_image):
if input_image is None:
return None
# 1. Prepare Paths
input_dir = os.path.join(COMFYUI_DIR, "input")
output_dir = os.path.join(COMFYUI_DIR, "output")
os.makedirs(input_dir, exist_ok=True)
os.makedirs(output_dir, exist_ok=True)
# Save input image with a fixed name for the workflow
input_filename = "input.png"
input_path = os.path.join(input_dir, input_filename)
input_image.save(input_path)
# 2. Launch ComfyUI (Headless)
print("Launching Headless ComfyUI server...")
# Using the correct CWD is critical for ComfyUI to find its models and custom nodes
cmd = [sys.executable, "main.py", "--listen", "127.0.0.1", "--port", "8188", "--disable-auto-launch"]
proc = subprocess.Popen(cmd, cwd=COMFYUI_DIR)
# Wait for server to be ready (increased timeout and added logging)
server_ready = False
for i in range(45): # 90 seconds max
try:
resp = requests.get("http://127.0.0.1:8188/history", timeout=2)
if resp.status_code == 200:
server_ready = True
print("ComfyUI server is ready!")
break
except:
if i % 5 == 0: print(f"Waiting for server... ({i*2}s)")
time.sleep(2)
if not server_ready:
print("Server logs (first 50 lines):")
# In a real environment, we'd capture stdout/stderr, but for now we'll just fail clearly
proc.terminate()
raise RuntimeError("ComfyUI server failed to start. Port 8188 remained closed.")
try:
# 3. Load pre-converted API workflow
workflow_path = os.path.join(ROOT_DIR, "simple_api_workflow.json")
with open(workflow_path, 'r') as f:
api_prompt = json.load(f)
# Update node 11 (LoadImage) to point to our input.png
if "11" in api_prompt:
api_prompt["11"]["inputs"]["image"] = input_filename
# Send to ComfyUI
print(f"Queueing workflow to ComfyUI ({len(api_prompt)} nodes)...")
prompt_data = {"prompt": api_prompt}
resp = requests.post("http://127.0.0.1:8188/prompt", json=prompt_data)
if resp.status_code != 200:
raise RuntimeError(f"Failed to queue prompt: {resp.text}")
prompt_id = resp.json().get("prompt_id")
print(f"Prompt queued successfully (ID: {prompt_id})")
# 4. Wait for completion
# We poll the history endpoint until the prompt_id appears
max_poll = 120 # 120 seconds for processing
finished = False
output_filename = None
for p in range(max_poll):
history_resp = requests.get(f"http://127.0.0.1:8188/history/{prompt_id}")
if history_resp.status_code == 200:
history = history_resp.json()
if prompt_id in history:
# Success!
print("Processing complete!")
# Extract output filename from the SaveImage node (ID 62)
output_data = history[prompt_id]['outputs'].get('62')
if output_data and 'images' in output_data:
output_filename = output_data['images'][0]['filename']
finished = True
break
if p % 10 == 0: print(f"Still processing... ({p}s)")
time.sleep(1)
if not finished:
raise RuntimeError("Processing timed out or failed to save image.")
# 5. Return result
output_path = os.path.join(output_dir, output_filename)
return Image.open(output_path).copy() # Copy to avoid library closing issues
finally:
print("Shutting down ComfyUI server...")
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
# Cleanup input file
if os.path.exists(input_path): os.remove(input_path)
# Premium UI with Fixed Height and No Share Buttons
css = """
#container {
max-width: 1200px;
margin: 0 auto;
}
.image-preview {
max-height: 512px !important;
}
footer {display: none !important;}
"""
with gr.Blocks(title="SynthID Remover") as demo:
with gr.Column(elem_id="container"):
gr.Markdown("# SynthID Remover")
gr.Markdown("This tool removes SynthID watermarks by re-rendering images through a high-fidelity diffusion reconstruction pipeline. It is specifically designed to bypass SynthID detection while maintaining the original image structure.")
with gr.Row():
with gr.Column():
input_img = gr.Image(type="pil", label="Input Image", height=512)
with gr.Column():
output_img = gr.Image(type="pil", label="Cleaned Image", height=512, interactive=False)
submit_btn = gr.Button("Remove Watermark", variant="primary")
submit_btn.click(
fn=remove_watermark,
inputs=[input_img],
outputs=[output_img]
)
with gr.Accordion("How it works & Acknowledgments", open=False):
gr.Markdown("""
### Acknowledgments
This project is a direct implementation of the research by [00quebec/Synthid-Bypass](https://github.com/00quebec/Synthid-Bypass). All credit for the discovery and the original ComfyUI workflows goes to the original authors.
### Technical Breakdown
The removal process works by re-processing the image through a specialized diffusion pipeline:
1. **Pixel Laundering**: The image is re-rendered using the **Z-Image-Turbo (S3-DiT)** model with a low denoising factor (0.2). This replaces the watermark's subtle noise patterns with new noise from the model.
2. **Structural Guidance**: To prevent the image from changing, a **Canny ControlNet** locks in the original geometry and composition.
3. **Multi-Pass Denoising**: The process runs in three iterative stages to gently scrub away the watermark without introducing artifacts.
4. **Face Restoration**: Using **FaceDetailer (YOLOv8)**, any detected faces are isolated and refined separately to preserve facial identity and high-end detail.
""")
if __name__ == "__main__":
# In Gradio 6.0+, css moved to launch(), but title remains in Blocks()
demo.launch(css=css)