diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..da1d86d --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +OPENROUTER_API_KEY=your-api-key-here diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a73210 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +ANALYZED_DO_NOT_SCAN_THIS_ONE/ +analyze_images.log diff --git a/abstract/wallhaven-je3qeq.jpg b/abstract_black_white_texture.jpg similarity index 100% rename from abstract/wallhaven-je3qeq.jpg rename to abstract_black_white_texture.jpg diff --git a/misc/fedora.png b/abstract_blue_curves.png similarity index 100% rename from misc/fedora.png rename to abstract_blue_curves.png diff --git a/misc/blue-abstract.jpg b/abstract_blue_fluid_art.jpg similarity index 100% rename from misc/blue-abstract.jpg rename to abstract_blue_fluid_art.jpg diff --git a/anime/wallhaven-3qq5j3.jpg b/angel_red_wings_sci_fi.jpg similarity index 100% rename from anime/wallhaven-3qq5j3.jpg rename to angel_red_wings_sci_fi.jpg diff --git a/misc/celestial-coronation.webp b/angels_crowning_figure.webp similarity index 100% rename from misc/celestial-coronation.webp rename to angels_crowning_figure.webp diff --git a/anime/power.png b/anime_girl_power_shirt.png similarity index 100% rename from anime/power.png rename to anime_girl_power_shirt.png diff --git a/antique/salzburg.png b/arched_view_city_sunset.png similarity index 100% rename from antique/salzburg.png rename to arched_view_city_sunset.png diff --git a/humanity/wallhaven-qrrlzr.jpg b/astronaut_earth_space.jpg similarity index 100% rename from humanity/wallhaven-qrrlzr.jpg rename to astronaut_earth_space.jpg diff --git a/humanity/astro.jpg b/astronaut_space_earth.jpg similarity index 100% rename from humanity/astro.jpg rename to astronaut_space_earth.jpg diff --git a/architecture/wallhaven-6ldqp7.jpg b/balcony_christmas_lights_building.jpg similarity index 100% rename from architecture/wallhaven-6ldqp7.jpg rename to balcony_christmas_lights_building.jpg diff --git a/nature/flowers_1.jpg b/blue_and_brown_flowers.jpg similarity index 100% rename from nature/flowers_1.jpg rename to blue_and_brown_flowers.jpg diff --git a/anime/wallhaven-x68k7l.jpg b/chainsaw_man_movie_theater.jpg similarity index 100% rename from anime/wallhaven-x68k7l.jpg rename to chainsaw_man_movie_theater.jpg diff --git a/framework-laptop16-wallpaper-pack/FW16-wallpaper-3.png b/chinese_characters_geometric_shapes.png similarity index 100% rename from framework-laptop16-wallpaper-pack/FW16-wallpaper-3.png rename to chinese_characters_geometric_shapes.png diff --git a/games/e33.png b/clair_obscur_inside_canvas.png similarity index 100% rename from games/e33.png rename to clair_obscur_inside_canvas.png diff --git a/games/wallhaven-xe593z.png b/clair_obscur_staring.png similarity index 100% rename from games/wallhaven-xe593z.png rename to clair_obscur_staring.png diff --git a/nature/wallhaven-3qkzqv.jpg b/dark_blue_clouds_contrail.jpg similarity index 100% rename from nature/wallhaven-3qkzqv.jpg rename to dark_blue_clouds_contrail.jpg diff --git a/games/wallhaven-e76ge8.jpg b/elden_ring_key_art.jpg similarity index 100% rename from games/wallhaven-e76ge8.jpg rename to elden_ring_key_art.jpg diff --git a/humanity/wallhaven-8g5w52.png b/falling_figure_black_background.png similarity index 100% rename from humanity/wallhaven-8g5w52.png rename to falling_figure_black_background.png diff --git a/nature/wallhaven-rqdklj.jpg b/foggy_forest_treetops.jpg similarity index 100% rename from nature/wallhaven-rqdklj.jpg rename to foggy_forest_treetops.jpg diff --git a/framework-laptop16-wallpaper-pack/FW16-wallpaper-6.png b/framework_laptop_exploded_view.png similarity index 100% rename from framework-laptop16-wallpaper-pack/FW16-wallpaper-6.png rename to framework_laptop_exploded_view.png diff --git a/misc/space.jpg b/galaxy_nebula_stars.jpg similarity index 100% rename from misc/space.jpg rename to galaxy_nebula_stars.jpg diff --git a/anime/wallhaven-1qjvx9.jpg b/girl_kimono_cat_jungle.jpg similarity index 100% rename from anime/wallhaven-1qjvx9.jpg rename to girl_kimono_cat_jungle.jpg diff --git a/anime/tv.jpg b/girl_tv_eyes_cables.jpg similarity index 100% rename from anime/tv.jpg rename to girl_tv_eyes_cables.jpg diff --git a/tech/wallhaven-ly3pzr.png b/hacker_ascii_art.png similarity index 100% rename from tech/wallhaven-ly3pzr.png rename to hacker_ascii_art.png diff --git a/games/wallhaven-gw7kde.png b/hollow_knight_mask_nail.png similarity index 100% rename from games/wallhaven-gw7kde.png rename to hollow_knight_mask_nail.png diff --git a/games/hornet.png b/hornet_hollow_knight_art.png similarity index 100% rename from games/hornet.png rename to hornet_hollow_knight_art.png diff --git a/games/wallhaven-7j2k89.png b/hornet_hollow_knight_cosplay.png similarity index 100% rename from games/wallhaven-7j2k89.png rename to hornet_hollow_knight_cosplay.png diff --git a/misc/store.jpg b/japanese_laundry_corner_night.jpg similarity index 100% rename from misc/store.jpg rename to japanese_laundry_corner_night.jpg diff --git a/anime/wallhaven-2ye9jm.jpg b/makima_convenience_store_night.jpg similarity index 100% rename from anime/wallhaven-2ye9jm.jpg rename to makima_convenience_store_night.jpg diff --git a/anime/makima.jpg b/makima_red_eyes_frame.jpg similarity index 100% rename from anime/makima.jpg rename to makima_red_eyes_frame.jpg diff --git a/humanity/wallhaven-5gwvz5.jpg b/moon_ship_field_person.jpg similarity index 100% rename from humanity/wallhaven-5gwvz5.jpg rename to moon_ship_field_person.jpg diff --git a/nature/wallhaven-8g5p52.jpg b/mount_fuji_black_white.jpg similarity index 100% rename from nature/wallhaven-8g5p52.jpg rename to mount_fuji_black_white.jpg diff --git a/anime/wallhaven-7j31qe.png b/nier_automata_2b_9s.png similarity index 100% rename from anime/wallhaven-7j31qe.png rename to nier_automata_2b_9s.png diff --git a/nature/flowers.jpg b/orange_and_pink_daisies.jpg similarity index 100% rename from nature/flowers.jpg rename to orange_and_pink_daisies.jpg diff --git a/architecture/wallhaven-8g5rrk.jpg b/ornate_greenhouse_statues.jpg similarity index 100% rename from architecture/wallhaven-8g5rrk.jpg rename to ornate_greenhouse_statues.jpg diff --git a/misc/storm-car.png b/pink_lightning_storm_night.png similarity index 100% rename from misc/storm-car.png rename to pink_lightning_storm_night.png diff --git a/tech/wallhaven-d8gwvj.png b/pixel_art_retro_icons.png similarity index 100% rename from tech/wallhaven-d8gwvj.png rename to pixel_art_retro_icons.png diff --git a/anime/wallhaven-3zx979.png b/power_red_eyes.png similarity index 100% rename from anime/wallhaven-3zx979.png rename to power_red_eyes.png diff --git a/nature/wallhaven-j5v28q.jpg b/red_eclipse_night_landscape.jpg similarity index 100% rename from nature/wallhaven-j5v28q.jpg rename to red_eclipse_night_landscape.jpg diff --git a/anime/guts.png b/red_guts_face.png similarity index 100% rename from anime/guts.png rename to red_guts_face.png diff --git a/scripts/analyze_images.py b/scripts/analyze_images.py new file mode 100755 index 0000000..15007a4 --- /dev/null +++ b/scripts/analyze_images.py @@ -0,0 +1,247 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "httpx", +# "pillow", +# "python-dotenv", +# "tqdm", +# ] +# /// +""" +Image analyzer script that uses OpenRouter AI to analyze and rename images. +Moves processed images to ANALYZED_DO_NOT_SCAN_THIS_ONE folder. +""" + +import base64 +import httpx +import logging +import os +import re +import sys +from pathlib import Path +from PIL import Image +import io +from dotenv import load_dotenv +from tqdm import tqdm + +# Load environment variables from .env file +load_dotenv() + +# Configure logging to file +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("analyze_images.log"), + ], +) +logger = logging.getLogger(__name__) + +# Configuration +OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY") +OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" +# Using a vision-capable model +MODEL = "google/gemini-2.5-flash" +OUTPUT_FOLDER = "ANALYZED_DO_NOT_SCAN_THIS_ONE" +IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"} + + +def get_image_base64(image_path: Path) -> tuple[str, str]: + """Read image and convert to base64, resizing if needed to reduce tokens.""" + with Image.open(image_path) as img: + # Convert to RGB if necessary (for PNG with transparency, etc.) + if img.mode in ("RGBA", "P"): + img = img.convert("RGB") + + # Resize if too large (max 1024px on longest side to save tokens) + max_size = 1024 + if max(img.size) > max_size: + ratio = max_size / max(img.size) + new_size = (int(img.width * ratio), int(img.height * ratio)) + img = img.resize(new_size, Image.Resampling.LANCZOS) + + # Save to bytes + buffer = io.BytesIO() + img.save(buffer, format="JPEG", quality=85) + buffer.seek(0) + + base64_data = base64.standard_b64encode(buffer.read()).decode("utf-8") + return base64_data, "image/jpeg" + + +def analyze_image(image_path: Path, client: httpx.Client) -> str | None: + """Send image to OpenRouter for analysis and get a descriptive filename.""" + try: + base64_data, media_type = get_image_base64(image_path) + except Exception as e: + logger.error(f"Error reading image {image_path}: {e}") + return None + + prompt = """Analyze this image and provide a short, descriptive filename for it. + +Rules for the filename: +1. Use lowercase letters only +2. Use underscores to separate words (no spaces or special characters) +3. Keep it concise but descriptive (3-6 words max) +4. Do NOT include the file extension +5. Describe the main subject/theme of the image + +Respond with ONLY the filename, nothing else. Example responses: +- sunset_over_mountains +- anime_girl_with_sword +- abstract_blue_waves +- cyberpunk_city_night""" + + payload = { + "model": MODEL, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": f"data:{media_type};base64,{base64_data}"}, + }, + {"type": "text", "text": prompt}, + ], + } + ], + "max_tokens": 50, + "temperature": 0.3, + } + + headers = { + "Authorization": f"Bearer {OPENROUTER_API_KEY}", + "Content-Type": "application/json", + "HTTP-Referer": "https://github.com/local-script", + "X-Title": "Image Analyzer Script", + } + + try: + response = client.post( + OPENROUTER_URL, json=payload, headers=headers, timeout=60.0 + ) + response.raise_for_status() + result = response.json() + + if "choices" in result and len(result["choices"]) > 0: + filename = result["choices"][0]["message"]["content"].strip() + # Clean up the filename + filename = re.sub(r"[^a-z0-9_]", "_", filename.lower()) + filename = re.sub(r"_+", "_", filename) # Remove multiple underscores + filename = filename.strip("_") + return filename + else: + logger.warning(f"Unexpected API response for {image_path}: {result}") + return None + + except httpx.HTTPStatusError as e: + logger.error( + f"API error for {image_path}: {e.response.status_code} - {e.response.text}" + ) + return None + except Exception as e: + logger.error(f"Error calling API for {image_path}: {e}") + return None + + +def find_images(root_dir: Path, exclude_folder: str) -> list[Path]: + """Find all image files in subdirectories, excluding the output folder.""" + images = [] + for path in root_dir.rglob("*"): + if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS: + # Skip files in the output folder or hidden directories + if exclude_folder not in path.parts and not any( + p.startswith(".") for p in path.parts + ): + images.append(path) + return sorted(images) + + +def get_unique_filename(output_dir: Path, base_name: str, extension: str) -> Path: + """Generate a unique filename by appending a number if needed.""" + candidate = output_dir / f"{base_name}{extension}" + if not candidate.exists(): + return candidate + + counter = 1 + while True: + candidate = output_dir / f"{base_name}_{counter}{extension}" + if not candidate.exists(): + return candidate + counter += 1 + + +def main(): + if not OPENROUTER_API_KEY: + print("Error: OPENROUTER_API_KEY environment variable is not set.") + print("Please set it with: export OPENROUTER_API_KEY='your-api-key'") + sys.exit(1) + + root_dir = Path.cwd() + output_dir = root_dir / OUTPUT_FOLDER + + # Create output directory if it doesn't exist + output_dir.mkdir(exist_ok=True) + logger.info(f"Output directory: {output_dir}") + + # Find all images + images = find_images(root_dir, OUTPUT_FOLDER) + + if not images: + print("No images found to process.") + logger.info("No images found to process.") + return + + print(f"Found {len(images)} images to analyze.") + logger.info(f"Found {len(images)} images to analyze.") + + # Process images + processed = 0 + failed = 0 + + with httpx.Client() as client: + for image_path in tqdm(images, desc="Analyzing images", unit="img"): + relative_path = image_path.relative_to(root_dir) + logger.info(f"Processing: {relative_path}") + + new_name = analyze_image(image_path, client) + + if new_name: + extension = image_path.suffix.lower() + new_path = get_unique_filename(output_dir, new_name, extension) + + # Move the file + try: + image_path.rename(new_path) + logger.info(f"Moved {relative_path} -> {new_path.name}") + processed += 1 + except Exception as e: + logger.error(f"Error moving file {relative_path}: {e}") + failed += 1 + else: + logger.warning(f"Skipped {relative_path} (analysis failed)") + failed += 1 + + print(f"\nDone! Processed: {processed}, Failed: {failed}") + logger.info(f"Completed. Processed: {processed}, Failed: {failed}") + + # Clean up empty directories + print("Cleaning up empty directories...") + for dirpath in sorted(root_dir.rglob("*"), reverse=True): + if dirpath.is_dir() and not any(dirpath.iterdir()) and dirpath != output_dir: + if not any(p.startswith(".") for p in dirpath.relative_to(root_dir).parts): + try: + dirpath.rmdir() + logger.info( + f"Removed empty directory: {dirpath.relative_to(root_dir)}" + ) + except Exception: + pass + + print(f"See analyze_images.log for detailed logs.") + + +if __name__ == "__main__": + main() diff --git a/anime/wallhaven-9odwod.png b/skull_berserk_brand_dark.png similarity index 100% rename from anime/wallhaven-9odwod.png rename to skull_berserk_brand_dark.png diff --git a/misc/min.png b/textured_cylinder_black_background.png similarity index 100% rename from misc/min.png rename to textured_cylinder_black_background.png diff --git a/misc/sunshine.webp b/twilight_rural_landscape.webp similarity index 100% rename from misc/sunshine.webp rename to twilight_rural_landscape.webp diff --git a/nature/cow.png b/ufo_cows_field.png similarity index 100% rename from nature/cow.png rename to ufo_cows_field.png diff --git a/games/wallhaven-zpmgwy.jpg b/underwater_ruins_kelp_forest.jpg similarity index 100% rename from games/wallhaven-zpmgwy.jpg rename to underwater_ruins_kelp_forest.jpg diff --git a/nature/wallhaven-p8gp83.jpg b/van_under_fiery_sky.jpg similarity index 100% rename from nature/wallhaven-p8gp83.jpg rename to van_under_fiery_sky.jpg