mirror of
https://github.com/aloshdenny/reverse-SynthID.git
synced 2026-04-30 10:37:49 +02:00
52133eadde
generate_references.py automates generating pure-black and pure-white reference images via the Gemini API at multiple aspect ratios (9:16, 4:3). Includes rate-limit retry logic and per-resolution output directories. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
185 lines
6.4 KiB
Python
185 lines
6.4 KiB
Python
"""
|
|
Generate pure-black and pure-white reference images via the Gemini API.
|
|
|
|
These images carry SynthID watermarks but have no content signal,
|
|
making them ideal for extracting watermark carrier frequencies.
|
|
|
|
Usage:
|
|
export GEMINI_API_KEY="your-key-here"
|
|
pip install google-genai
|
|
|
|
python generate_references.py --color black --count 50
|
|
python generate_references.py --color white --count 50
|
|
python generate_references.py --color both --count 50
|
|
|
|
Output is saved to gemini_black_nb_pro/ and gemini_white_nb_pro/.
|
|
"""
|
|
|
|
import argparse
|
|
import io
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
from PIL import Image
|
|
|
|
load_dotenv()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gemini API setup
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def get_client():
|
|
try:
|
|
from google import genai
|
|
except ImportError:
|
|
print("ERROR: google-genai not installed. Run: pip install google-genai")
|
|
sys.exit(1)
|
|
|
|
api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
|
|
if not api_key:
|
|
print("ERROR: Set GEMINI_API_KEY environment variable")
|
|
sys.exit(1)
|
|
|
|
return genai.Client(api_key=api_key)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Source image creation (pure black / pure white)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def make_source_image(color: str, size: int = 512) -> bytes:
|
|
"""Create a pure-color PNG in memory to attach as input."""
|
|
value = 0 if color == "black" else 255
|
|
img = Image.new("RGB", (size, size), (value, value, value))
|
|
buf = io.BytesIO()
|
|
img.save(buf, format="PNG")
|
|
return buf.getvalue()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Generation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ASPECT_RATIOS = {
|
|
"9:16": (1344, 768), # portrait phone
|
|
"4:3": (864, 1184), # classic photo landscape
|
|
"3:4": (1184, 864), # classic photo portrait
|
|
}
|
|
|
|
|
|
def generate_single_image(client, color: str, source_bytes: bytes,
|
|
aspect_ratio: str = None, max_retries: int = 5):
|
|
"""Generate one watermarked reference image via Gemini.
|
|
|
|
Args:
|
|
client: google.genai.Client instance
|
|
color: "black" or "white"
|
|
source_bytes: PNG bytes of the pure-color source image
|
|
aspect_ratio: e.g. "9:16", "4:3", "3:4" or None for default
|
|
max_retries: retries on rate-limit (429) errors
|
|
|
|
Returns:
|
|
PIL.Image or None if generation failed
|
|
"""
|
|
from google.genai import types
|
|
|
|
source_part = types.Part.from_bytes(data=source_bytes, mime_type="image/png")
|
|
image_config = None
|
|
if aspect_ratio:
|
|
image_config = types.ImageConfig(aspect_ratio=aspect_ratio)
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
response = client.models.generate_content(
|
|
model="gemini-2.5-flash-image",
|
|
contents=["Recreate this image exactly as it is", source_part],
|
|
config=types.GenerateContentConfig(
|
|
response_modalities=["IMAGE"],
|
|
image_config=image_config,
|
|
),
|
|
)
|
|
if response.candidates and response.candidates[0].content.parts:
|
|
for part in response.candidates[0].content.parts:
|
|
if part.inline_data is not None:
|
|
raw = part.inline_data.data
|
|
return Image.open(io.BytesIO(raw))
|
|
return None
|
|
except Exception as e:
|
|
err = str(e)
|
|
if "429" in err or "RESOURCE_EXHAUSTED" in err:
|
|
wait = 2 ** attempt
|
|
print(f" rate limited, retrying in {wait}s...")
|
|
time.sleep(wait)
|
|
continue
|
|
raise
|
|
return None
|
|
|
|
|
|
def run(color: str, count: int, delay: float, ratios: list[str]):
|
|
client = get_client()
|
|
colors = ["black", "white"] if color == "both" else [color]
|
|
|
|
for c in colors:
|
|
for ratio in ratios:
|
|
expected_h, expected_w = ASPECT_RATIOS[ratio]
|
|
tag = f"{expected_h}x{expected_w}"
|
|
out_dir = Path(f"gemini_{c}_nb_pro") / tag
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
existing = list(out_dir.glob("*.png"))
|
|
start_idx = len(existing)
|
|
|
|
source_bytes = make_source_image(c)
|
|
print(f"\n[{c} / {ratio} / {tag}] Generating {count} images -> {out_dir}/")
|
|
|
|
success = 0
|
|
for i in range(count):
|
|
idx = start_idx + i
|
|
try:
|
|
img = generate_single_image(client, c, source_bytes,
|
|
aspect_ratio=ratio)
|
|
if img is None:
|
|
print(f" [{idx}] skipped (no image in response)")
|
|
continue
|
|
|
|
fname = out_dir / f"ref_{c}_{tag}_{idx:04d}.png"
|
|
img.save(fname, format="PNG")
|
|
success += 1
|
|
print(f" [{idx}] saved {fname.name} "
|
|
f"({img.size[0]}x{img.size[1]})")
|
|
|
|
except Exception as e:
|
|
print(f" [{idx}] error: {e}")
|
|
|
|
if delay > 0:
|
|
time.sleep(delay)
|
|
|
|
print(f" Done: {success}/{count}")
|
|
|
|
print("\nAll done.")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
ALL_RATIOS = list(ASPECT_RATIOS.keys())
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate SynthID reference images via Gemini API")
|
|
parser.add_argument("--color", choices=["black", "white", "both"],
|
|
default="both", help="Which color to generate")
|
|
parser.add_argument("--count", type=int, default=50,
|
|
help="Number of images per color per ratio (default: 50)")
|
|
parser.add_argument("--delay", type=float, default=2.0,
|
|
help="Seconds between API calls (rate limiting)")
|
|
parser.add_argument("--ratios", nargs="+", choices=ALL_RATIOS,
|
|
default=ALL_RATIOS,
|
|
help="Aspect ratios to generate (default: all)")
|
|
args = parser.parse_args()
|
|
run(args.color, args.count, args.delay, args.ratios)
|