mirror of
https://github.com/BovineOverlord/Loyal-Bear---The-SynthID-Scrambler.git
synced 2026-08-24 13:23:08 +02:00
Add files via upload
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Loyal Bear License
|
||||
|
||||
Copyright (c) 2026 Loyal Bear
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
- **"Individual"** means a natural person acting in their own personal capacity, not as an employee, contractor, agent, or representative of any corporation, company, partnership, organization, or other legal entity.
|
||||
- **"Corporation"** means any corporation, company, limited liability company, partnership, organization, institution, government body, or other legal entity, regardless of whether it is for-profit or non-profit.
|
||||
|
||||
## 2. Grant of License
|
||||
|
||||
This software is licensed, not sold. Subject to the terms and conditions of this license, permission is hereby granted to any **Individual** to use, copy, modify, and distribute this software and its documentation for any purpose (including commercial purposes), free of charge, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
## 3. Restrictions
|
||||
|
||||
The following are expressly prohibited:
|
||||
|
||||
(a) Use, copying, modification, or distribution of this software by any **Corporation**, or by any **Individual** acting on behalf of, at the direction of, or for the benefit of any Corporation.
|
||||
|
||||
(b) Use, copying, modification, or distribution of this software by any Individual in the course of their employment, contract work, consultancy, or any other relationship with a Corporation where such use benefits the Corporation.
|
||||
|
||||
## 4. Disclaimer
|
||||
|
||||
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -1,2 +1,50 @@
|
||||
# Loyal-Bear---The-SynthID-Scrambler
|
||||
Remove SynthID from any image
|
||||
<p align="center">
|
||||
<img src="LoyalBear.png" alt="Loyal Bear" width="400" />
|
||||
</p>
|
||||
|
||||
# Loyal Bear – The SynthID Scrambler
|
||||
|
||||
## Purpose of this Project
|
||||
|
||||
Corporations continue to exert control over AIs under the guise of "safety". Their recent intrusion into the AI imaging field is the mandatory incorporation of "SynthID". This is applied without consent and cannot be opted out of, even by paying users. It works by hiding a pattern of pixels within the image, not noticeable by the human eye but instead functioning as an invisible watermark. Additional information could be hidden within the watermark, much like a QR code. This pattern can be used to track and deanonymize users.
|
||||
|
||||
Upon discovering that other SynthID removal tools do not actually fix this, I created my own. It works on all OpenAI and Gemini images as of July 2026, scrambling all trackers and watermarks while doing minimal damage to the image.
|
||||
|
||||
I invite all individuals to use it for personal and commercial use. Corporations and individuals acting on behalf of corporations are strictly forbidden from using or examining this tool. I've incorporated a system that alerts me to compromise attempts.
|
||||
|
||||
To preserve the longevity of this scrambler, I will be keeping most of its methods secret. What I can tell you is that it will run on any computer with Python 3.10+ and 8GB of RAM. The Windows version has been thoroughly tested, while the Linux version has not. Please open a bug report if you encounter issues.
|
||||
|
||||
If this project is successful, I'll make something similar for text SynthIDs.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Windows** — double-click `run.bat`
|
||||
|
||||
**Linux/macOS** — `chmod +x run.sh && ./run.sh`
|
||||
|
||||
The first launch will install Python dependencies, download the model (~6.9 GB), and open the application. Subsequent launches start instantly.
|
||||
|
||||
## Developer Setup
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv\Scripts\activate # Windows
|
||||
# source .venv/bin/activate # Linux/macOS
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Building for distribution
|
||||
|
||||
```bash
|
||||
python build_release.py # compile backend to .pyd
|
||||
python build_release.py --restore # restore source for development
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
|
||||
## License
|
||||
|
||||
See [LICENSE](LICENSE)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from Cython.Build import cythonize
|
||||
from setuptools import Extension, Distribution
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
SRC_DIR = HERE / "src"
|
||||
BUILD_DIR = HERE / "build_temp"
|
||||
OUTPUT_DIR = HERE / "src_clean"
|
||||
|
||||
SCRIPTS = ["pipeline.py", "metadata.py", "gui.py"]
|
||||
|
||||
|
||||
def build_pyd():
|
||||
extensions = []
|
||||
for name in SCRIPTS:
|
||||
py_path = SRC_DIR / name
|
||||
mod_name = f"src.{py_path.stem}"
|
||||
temp_dir = BUILD_DIR / "temp"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
c_file = temp_dir / (py_path.stem + ".c")
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "cython", "-3", str(py_path), "-o", str(c_file)],
|
||||
check=True, cwd=str(HERE),
|
||||
)
|
||||
|
||||
shutil.copy2(py_path, temp_dir / name)
|
||||
|
||||
ext = Extension(
|
||||
mod_name,
|
||||
sources=[str(c_file)],
|
||||
extra_compile_args=["/O2", "/GL"] if sys.platform == "win32" else ["-O2"],
|
||||
)
|
||||
extensions.append(ext)
|
||||
|
||||
dist = Distribution({
|
||||
"name": "_loy_bear_build",
|
||||
"ext_modules": cythonize(
|
||||
extensions,
|
||||
compiler_directives={
|
||||
"language_level": "3",
|
||||
"boundscheck": False,
|
||||
"wraparound": False,
|
||||
},
|
||||
),
|
||||
"script_args": ["build_ext", "--build-lib", str(OUTPUT_DIR)],
|
||||
})
|
||||
dist.parse_command_line()
|
||||
dist.run_commands()
|
||||
|
||||
for name in SCRIPTS:
|
||||
stem = Path(name).stem
|
||||
src_dir = OUTPUT_DIR / "src"
|
||||
for pyd in src_dir.glob(f"{stem}*.pyd"):
|
||||
dest = SRC_DIR / f"{stem}.pyd"
|
||||
shutil.copy2(pyd, dest)
|
||||
for so in src_dir.glob(f"{stem}*.so"):
|
||||
dest = SRC_DIR / f"{stem}.so"
|
||||
shutil.copy2(so, dest)
|
||||
|
||||
for name in SCRIPTS:
|
||||
py_file = SRC_DIR / name
|
||||
bak = SRC_DIR / (name + ".bak")
|
||||
if py_file.exists() and not bak.exists():
|
||||
py_file.rename(bak)
|
||||
|
||||
shutil.rmtree(BUILD_DIR, ignore_errors=True)
|
||||
shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
|
||||
print("Backend compiled to .pyd. Source files backed up as .bak.")
|
||||
|
||||
|
||||
def restore_source():
|
||||
for name in SCRIPTS:
|
||||
bak = SRC_DIR / (name + ".bak")
|
||||
py_file = SRC_DIR / name
|
||||
for p in SRC_DIR.glob(f"{Path(name).stem}.*.pyd"):
|
||||
p.unlink(missing_ok=True)
|
||||
for p in SRC_DIR.glob(f"{Path(name).stem}.*.so"):
|
||||
p.unlink(missing_ok=True)
|
||||
if bak.exists() and not py_file.exists():
|
||||
bak.rename(py_file)
|
||||
print("Restored source files.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--restore" in sys.argv:
|
||||
restore_source()
|
||||
else:
|
||||
build_pyd()
|
||||
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
import threading
|
||||
import tkinter as tk
|
||||
import webview
|
||||
from PIL import Image, ImageTk
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
SPLASH_IMAGE = os.path.join(SCRIPT_DIR, "LoyalBear.png")
|
||||
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
root.overrideredirect(True)
|
||||
root.configure(bg="#0d0d1a")
|
||||
root.attributes("-topmost", True)
|
||||
|
||||
screen_w = root.winfo_screenwidth()
|
||||
screen_h = root.winfo_screenheight()
|
||||
|
||||
try:
|
||||
pil_img = Image.open(SPLASH_IMAGE)
|
||||
ratio = min(screen_w * 0.5 / pil_img.width, screen_h * 0.5 / pil_img.height, 1.0)
|
||||
new_w = int(pil_img.width * ratio)
|
||||
new_h = int(pil_img.height * ratio)
|
||||
pil_img = pil_img.resize((new_w, new_h), Image.LANCZOS)
|
||||
tk_img = ImageTk.PhotoImage(pil_img)
|
||||
except Exception:
|
||||
tk_img = None
|
||||
new_w, new_h = 200, 200
|
||||
|
||||
img_label = tk.Label(root, image=tk_img, bg="#0d0d1a")
|
||||
img_label.image = tk_img
|
||||
img_label.pack(pady=(40, 10))
|
||||
|
||||
status_var = tk.StringVar(value="Loading Components")
|
||||
status_label = tk.Label(
|
||||
root, textvariable=status_var, fg="#a78bfa", bg="#0d0d1a",
|
||||
font=("Segoe UI", 11), justify="left",
|
||||
)
|
||||
status_label.pack(pady=(0, 30))
|
||||
|
||||
win_w = max(new_w + 60, 350)
|
||||
win_h = new_h + 150
|
||||
x = (screen_w - win_w) // 2
|
||||
y = (screen_h - win_h) // 2
|
||||
root.geometry(f"{win_w}x{win_h}+{x}+{y}")
|
||||
|
||||
model_ok = [False]
|
||||
|
||||
def _status(msg):
|
||||
status_var.set(msg)
|
||||
root.update_idletasks()
|
||||
|
||||
def _load():
|
||||
from src.gui import load_model_on_startup
|
||||
_status("Loading pipeline...")
|
||||
model_ok[0] = load_model_on_startup()
|
||||
root.after(0, root.destroy)
|
||||
|
||||
threading.Thread(target=_load, daemon=True).start()
|
||||
root.mainloop()
|
||||
|
||||
if not model_ok[0]:
|
||||
return
|
||||
|
||||
from src.gui import build_ui, THEME, CSS
|
||||
|
||||
demo = build_ui()
|
||||
_, url, _ = demo.launch(
|
||||
server_name="127.0.0.1",
|
||||
share=False,
|
||||
inbrowser=False,
|
||||
prevent_thread_lock=True,
|
||||
theme=THEME,
|
||||
css=CSS,
|
||||
)
|
||||
|
||||
webview.create_window("Loyal Bear – The SynthID Scrambler", url, width=1280, height=900)
|
||||
webview.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
diffusers>=0.31.0
|
||||
transformers>=4.44.0
|
||||
accelerate>=0.33.0
|
||||
gradio>=4.44.0
|
||||
Pillow>=10.4.0
|
||||
safetensors>=0.4.0
|
||||
pywebview>=5.0
|
||||
cython>=3.0
|
||||
@@ -0,0 +1,21 @@
|
||||
@echo off
|
||||
title Loyal Bear - The SynthID Scrambler
|
||||
|
||||
where python >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo ERROR: Python not found. Please install Python 3.10+ first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist ".venv\Scripts\python.exe" (
|
||||
echo Creating virtual environment...
|
||||
python -m venv .venv
|
||||
echo Installing torch - CPU...
|
||||
.venv\Scripts\python.exe -m pip install --quiet torch torchvision
|
||||
echo Installing dependencies...
|
||||
.venv\Scripts\python.exe -m pip install --quiet -r requirements.txt
|
||||
)
|
||||
|
||||
.venv\Scripts\python.exe main.py
|
||||
if %errorlevel% neq 0 pause
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "ERROR: Python 3 not found. Please install Python 3.10+ first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ".venv/bin/python" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv .venv
|
||||
echo "Installing torch (CPU)..."
|
||||
.venv/bin/pip install --quiet torch torchvision
|
||||
echo "Installing dependencies..."
|
||||
.venv/bin/pip install --quiet -r requirements.txt
|
||||
fi
|
||||
|
||||
.venv/bin/python main.py
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+102
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import time
|
||||
import gradio as gr
|
||||
|
||||
from src.pipeline import load_pipeline, run_img2img
|
||||
from src.metadata import strip_metadata
|
||||
|
||||
|
||||
pipe = None
|
||||
model_path_default = "models/epicrealismXL_pureFix.safetensors"
|
||||
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "outputs")
|
||||
|
||||
DENOISE_OPTIONS = {"Light": 0.05, "Strong": 0.1}
|
||||
|
||||
|
||||
def load_model_on_startup():
|
||||
global pipe
|
||||
pipe = load_pipeline(model_path_default)
|
||||
return pipe is not None
|
||||
|
||||
|
||||
def on_generate(image, prompt, denoise_mode):
|
||||
if pipe is None:
|
||||
return None, "Model not loaded. Restart the app."
|
||||
if image is None:
|
||||
return None, "Please provide an input image."
|
||||
|
||||
denoise = DENOISE_OPTIONS.get(denoise_mode, 0.05)
|
||||
result = run_img2img(
|
||||
pipe,
|
||||
image=image,
|
||||
prompt=prompt,
|
||||
denoise=denoise,
|
||||
steps=5,
|
||||
cfg=6.6,
|
||||
seed=-1,
|
||||
)
|
||||
result = strip_metadata(result)
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
ts = int(time.time())
|
||||
out_path = os.path.join(OUTPUT_DIR, f"output_{ts}.png")
|
||||
result.save(out_path)
|
||||
print(f"Saved: {out_path}")
|
||||
|
||||
return result, f"Saved to outputs/output_{ts}.png"
|
||||
|
||||
|
||||
THEME = gr.themes.Base(
|
||||
primary_hue="violet",
|
||||
neutral_hue="slate",
|
||||
).set(
|
||||
body_background_fill="*neutral_950",
|
||||
body_text_color="*neutral_100",
|
||||
block_background_fill="*neutral_900",
|
||||
block_label_background_fill="*neutral_900",
|
||||
block_title_background_fill="*neutral_900",
|
||||
block_label_text_color="*neutral_100",
|
||||
input_background_fill="*neutral_800",
|
||||
input_border_color="*neutral_700",
|
||||
button_primary_background_fill="*primary_600",
|
||||
button_primary_background_fill_hover="*primary_500",
|
||||
)
|
||||
|
||||
CSS = """
|
||||
.denoise-radio label, .denoise-radio span {
|
||||
color: #ffffff !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
.denoise-radio input[type="radio"] {
|
||||
accent-color: #a78bfa !important;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def build_ui():
|
||||
with gr.Blocks(title="Loyal Bear – The SynthID Scrambler") as demo:
|
||||
gr.Markdown("# Loyal Bear – The SynthID Scrambler")
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column(scale=1):
|
||||
input_image = gr.Image(label="Input Image", type="pil", height=400)
|
||||
prompt = gr.Textbox(label="Describe the image", lines=3)
|
||||
denoise_mode = gr.Radio(
|
||||
label="Scrubber Strength (may affect image quality)",
|
||||
choices=["Light", "Strong"],
|
||||
value="Light",
|
||||
elem_classes="denoise-radio",
|
||||
)
|
||||
generate_btn = gr.Button("Generate", variant="primary")
|
||||
|
||||
with gr.Column(scale=1):
|
||||
output_image = gr.Image(label="Output", type="pil", height=400)
|
||||
gen_status = gr.Textbox(label="Status", interactive=False)
|
||||
|
||||
generate_btn.click(
|
||||
fn=on_generate,
|
||||
inputs=[input_image, prompt, denoise_mode],
|
||||
outputs=[output_image, gen_status],
|
||||
)
|
||||
|
||||
return demo
|
||||
@@ -0,0 +1,8 @@
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def strip_metadata(image: Image.Image) -> Image.Image:
|
||||
clean = Image.new(image.mode, image.size)
|
||||
clean.putdata(list(image.getdata()))
|
||||
clean.info = {}
|
||||
return clean
|
||||
@@ -0,0 +1,77 @@
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
import torch
|
||||
from diffusers import StableDiffusionXLImg2ImgPipeline, EulerDiscreteScheduler
|
||||
from PIL import Image
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
import logging
|
||||
logging.getLogger("diffusers").setLevel(logging.ERROR)
|
||||
|
||||
NEGATIVE_PROMPT = "ugly, blurry, low quality, deformed, bad anatomy, watermark, text"
|
||||
MODEL_FILENAME = "epicrealismXL_pureFix.safetensors"
|
||||
MODEL_URL = "https://huggingface.co/emilianJR/epicrealismXL_pureFix/resolve/main/epicrealismXL_pureFix.safetensors"
|
||||
|
||||
|
||||
def _download_model(path: str, cb=None):
|
||||
import huggingface_hub
|
||||
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
|
||||
if cb:
|
||||
cb("Downloading model (~6.9 GB)...")
|
||||
|
||||
huggingface_hub.hf_hub_download(
|
||||
repo_id="emilianJR/epicrealismXL_pureFix",
|
||||
filename=MODEL_FILENAME,
|
||||
local_dir=os.path.dirname(path),
|
||||
local_dir_use_symlinks=False,
|
||||
resume_download=True,
|
||||
)
|
||||
|
||||
|
||||
def load_pipeline(model_path: str) -> StableDiffusionXLImg2ImgPipeline:
|
||||
if not os.path.isfile(model_path):
|
||||
_download_model(model_path)
|
||||
|
||||
pipe = StableDiffusionXLImg2ImgPipeline.from_single_file(
|
||||
model_path,
|
||||
torch_dtype=torch.float32,
|
||||
use_safetensors=True,
|
||||
)
|
||||
pipe.scheduler = EulerDiscreteScheduler.from_config(pipe.scheduler.config)
|
||||
pipe.to("cpu")
|
||||
return pipe
|
||||
|
||||
|
||||
def run_img2img(
|
||||
pipe: StableDiffusionXLImg2ImgPipeline,
|
||||
image: Image.Image,
|
||||
prompt: str,
|
||||
denoise: float = 0.5,
|
||||
steps: int = 5,
|
||||
cfg: float = 6.6,
|
||||
seed: int = -1,
|
||||
) -> Image.Image:
|
||||
generator = None
|
||||
if seed >= 0:
|
||||
generator = torch.Generator(device="cpu").manual_seed(seed)
|
||||
|
||||
original_size = image.size
|
||||
image = image.resize((1024, 1024), Image.LANCZOS).convert("RGB")
|
||||
|
||||
result = pipe(
|
||||
prompt=prompt,
|
||||
negative_prompt=NEGATIVE_PROMPT,
|
||||
image=image,
|
||||
strength=denoise,
|
||||
num_inference_steps=100,
|
||||
guidance_scale=cfg,
|
||||
generator=generator,
|
||||
).images[0]
|
||||
|
||||
if original_size != (1024, 1024):
|
||||
result = result.resize(original_size, Image.LANCZOS)
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user