Files
rewards-farmer/src/random_image_for_visual_search.py
T

453 lines
8.7 KiB
Python
Raw Normal View History

import io
import json
import logging
import time
from pathlib import Path
from urllib.parse import urlsplit, urlunsplit
import requests
from PIL import Image
logger = logging.getLogger(__name__)
# ============================================================
# CONFIG
# ============================================================
API_URL = "https://commons.wikimedia.org/w/api.php"
2026-08-25 22:09:50 -04:00
OUTPUT_FILE = Path("visual_search.jpg")
2026-08-25 22:14:13 -04:00
METADATA_FILE = Path("visual_search.json")
MAX_ATTEMPTS = 10
# Loose filtering
MIN_WIDTH = 300
MIN_HEIGHT = 300
# Maximum original file size we'll accept
MAX_FILE_SIZE = 20 * 1024 * 1024 # 20 MB
# Wikimedia will generate a thumbnail around this width
THUMBNAIL_WIDTH = 1280
# Normal delay between failed attempts
REQUEST_DELAY = 3
# Maximum rate-limit wait
MAX_BACKOFF = 300
# ============================================================
# HTTP SESSION
# ============================================================
session = requests.Session()
session.headers.update({
2026-09-07 10:13:24 -04:00
"User-Agent": (
"RandomVisualSearchImage/1.1 "
"(contact: 12345rfdz@gmail.com)"
)
})
# ============================================================
# URL HELPERS
# ============================================================
def clean_url(url):
2026-09-07 10:13:24 -04:00
"""Remove query parameters from Wikimedia URLs."""
2026-09-07 10:13:24 -04:00
parts = urlsplit(url)
2026-09-07 10:13:24 -04:00
return urlunsplit((
parts.scheme,
parts.netloc,
parts.path,
"",
"",
))
# ============================================================
# RATE LIMIT HANDLING
# ============================================================
def wait_after_429(response, attempt):
2026-09-07 10:13:24 -04:00
"""Wait according to Wikimedia's Retry-After header."""
2026-09-07 10:13:24 -04:00
retry_after = response.headers.get("Retry-After")
2026-09-07 10:13:24 -04:00
if retry_after:
try:
wait_time = int(retry_after)
except ValueError:
wait_time = min(
2 ** attempt,
MAX_BACKOFF,
)
else:
wait_time = min(
2 ** attempt,
MAX_BACKOFF,
)
2026-09-07 10:13:24 -04:00
wait_time = max(5, wait_time)
logger.warning("Rate limited. Waiting %d seconds...", wait_time)
2026-09-07 10:13:24 -04:00
time.sleep(wait_time)
# ============================================================
# DOWNLOAD
# ============================================================
def download_image(url):
2026-09-07 10:13:24 -04:00
"""Download image bytes from Wikimedia."""
2026-09-07 10:13:24 -04:00
url = clean_url(url)
2026-09-07 10:13:24 -04:00
try:
response = session.get(
url,
timeout=30,
allow_redirects=True,
)
except (requests.RequestException, ConnectionResetError, OSError) as e:
logger.warning("Download failed: %s", e)
2026-09-07 10:13:24 -04:00
return None
2026-09-07 10:13:24 -04:00
if response.status_code == 429:
wait_after_429(response, 1)
return None
2026-09-07 10:13:24 -04:00
if response.status_code == 403:
logger.warning("Wikimedia returned 403 Forbidden.")
2026-09-07 10:13:24 -04:00
return None
2026-09-07 10:13:24 -04:00
try:
response.raise_for_status()
except requests.RequestException as e:
logger.warning("HTTP error: %s", e)
2026-09-07 10:13:24 -04:00
return None
2026-09-07 10:13:24 -04:00
content_type = response.headers.get(
"Content-Type",
"",
).lower()
2026-09-07 10:13:24 -04:00
if not content_type.startswith("image/"):
logger.warning("Not an image: %s", content_type)
2026-09-07 10:13:24 -04:00
return None
2026-09-07 10:13:24 -04:00
if not response.content:
logger.warning("Downloaded image is empty.")
2026-09-07 10:13:24 -04:00
return None
2026-09-07 10:13:24 -04:00
return response.content
# ============================================================
2026-08-25 22:09:50 -04:00
# JPEG CONVERSION
# ============================================================
2026-08-25 22:09:50 -04:00
def convert_to_jpeg(image_data):
2026-09-07 10:13:24 -04:00
"""Convert downloaded image bytes to JPEG."""
try:
with Image.open(
io.BytesIO(image_data)
) as image:
# JPEG does not support alpha (transparency).
# If the image has transparency (RGBA or LA), paste it over a white background.
if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
background = Image.new("RGB", image.size, (255, 255, 255))
if image.mode == "P":
image = image.convert("RGBA")
background.paste(image, mask=image.split()[-1])
jpeg_image = background
else:
jpeg_image = image.convert("RGB")
output = io.BytesIO()
jpeg_image.save(
output,
format="JPEG",
quality=90,
optimize=True,
)
return output.getvalue()
except Exception as e:
logger.warning("JPEG conversion failed: %s", e)
2026-09-07 10:13:24 -04:00
return None
def generate_fallback_image():
"""Generate a synthetic local JPEG image using PIL as a fallback."""
logger.info("Generating synthetic local fallback image for visual search...")
from PIL import ImageDraw
import random
img = Image.new("RGB", (800, 600), color=(random.randint(50, 200), random.randint(50, 200), random.randint(50, 200)))
draw = ImageDraw.Draw(img)
for _ in range(10):
x0 = random.randint(0, 700)
y0 = random.randint(0, 500)
x1 = x0 + random.randint(50, 200)
y1 = y0 + random.randint(50, 200)
fill = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
draw.rectangle([x0, y0, x1, y1], fill=fill)
output = io.BytesIO()
img.save(output, format="JPEG", quality=90)
jpeg_data = output.getvalue()
OUTPUT_FILE.write_bytes(jpeg_data)
logger.info("Saved fallback image to %s", OUTPUT_FILE.absolute())
return {"title": "Fallback Synthetic Image", "width": 800, "height": 600}
# ============================================================
# RANDOM IMAGE
# ============================================================
def get_random_image():
2026-09-07 10:13:24 -04:00
for attempt in range(
1,
MAX_ATTEMPTS + 1,
):
if attempt > 1:
time.sleep(REQUEST_DELAY)
logger.debug("Attempt %d/%d", attempt, MAX_ATTEMPTS)
2026-09-07 10:13:24 -04:00
params = {
"action": "query",
"format": "json",
"generator": "random",
"grnnamespace": 6,
"grnlimit": 1,
"prop": "imageinfo",
"iiprop": (
"url|size|mime|dimensions"
),
"iiurlwidth": THUMBNAIL_WIDTH,
}
try:
response = session.get(
API_URL,
params=params,
timeout=20,
)
except (requests.RequestException, ConnectionResetError, OSError) as e:
logger.warning("API request failed: %s", e)
2026-09-07 10:13:24 -04:00
continue
if response.status_code == 429:
wait_after_429(
response,
attempt,
)
continue
try:
response.raise_for_status()
data = response.json()
except (
requests.RequestException,
ValueError,
) as e:
logger.warning("API error: %s", e)
2026-09-07 10:13:24 -04:00
continue
pages = (
data
.get("query", {})
.get("pages", {})
)
if not pages:
logger.debug("No page returned.")
2026-09-07 10:13:24 -04:00
continue
page = next(
iter(pages.values())
)
title = page.get(
"title",
"Unknown",
)
imageinfo = page.get(
"imageinfo"
)
if not imageinfo:
logger.debug("No image information.")
2026-09-07 10:13:24 -04:00
continue
info = imageinfo[0]
mime = info.get(
"mime",
"",
)
width = info.get(
"width",
0,
)
height = info.get(
"height",
0,
)
size = info.get(
"size",
0,
)
thumbnail_url = info.get(
"thumburl"
)
original_url = info.get(
"url"
)
if mime not in {
"image/jpeg",
"image/png",
"image/webp",
}:
logger.debug("Skipping unsupported type: %s", mime)
2026-09-07 10:13:24 -04:00
continue
if width < MIN_WIDTH or height < MIN_HEIGHT:
logger.debug("Skipping small image: %dx%d", width, height)
2026-09-07 10:13:24 -04:00
continue
if size > MAX_FILE_SIZE:
logger.debug("Skipping large image: %.1f MB", size / 1024 / 1024)
2026-09-07 10:13:24 -04:00
continue
if not thumbnail_url:
logger.debug("No thumbnail URL.")
2026-09-07 10:13:24 -04:00
continue
logger.debug("Found: %s (%dx%d)", title, width, height)
2026-09-07 10:13:24 -04:00
image_data = download_image(
thumbnail_url
)
if image_data is None and original_url:
logger.debug("Thumbnail download failed, trying original URL...")
2026-09-07 10:13:24 -04:00
image_data = download_image(
original_url
)
if image_data is None:
logger.debug("Couldn't download image.")
2026-09-07 10:13:24 -04:00
continue
logger.debug("Converting to JPEG...")
2026-09-07 10:13:24 -04:00
jpeg_data = convert_to_jpeg(
image_data
)
if jpeg_data is None:
continue
try:
OUTPUT_FILE.write_bytes(
jpeg_data
)
except OSError as e:
logger.warning("Couldn't save image: %s", e)
2026-09-07 10:13:24 -04:00
continue
metadata = {
"title": title,
"source": "Wikimedia Commons",
"output_format": "JPEG",
"width": width,
"height": height,
"original_mime": mime,
"original_size": size,
"jpeg_size": len(
jpeg_data
),
"original_url": (
clean_url(original_url)
if original_url
else None
),
"thumbnail_url": (
clean_url(thumbnail_url)
if thumbnail_url
else None
),
}
try:
METADATA_FILE.write_text(
json.dumps(
metadata,
indent=4,
ensure_ascii=False,
),
encoding="utf-8",
)
except OSError as e:
logger.warning("Couldn't save metadata: %s", e)
logger.info(
"Visual search image saved: %s (%s, %.1f KB, source: %s)",
OUTPUT_FILE.absolute(),
f"{width}x{height}",
len(jpeg_data) / 1024,
title,
2026-09-07 10:13:24 -04:00
)
return metadata
logger.warning("Could not download image from Wikimedia Commons. Generating local fallback image.")
return generate_fallback_image()
# ============================================================
# MAIN
# ============================================================
if __name__ == "__main__":
2026-09-07 10:13:24 -04:00
get_random_image()