diff --git a/data/classic_eyes_ppm/classic_anime_amber.ppm b/data/classic_eyes_ppm/classic_anime_amber.ppm new file mode 100644 index 0000000..da6b5af Binary files /dev/null and b/data/classic_eyes_ppm/classic_anime_amber.ppm differ diff --git a/data/classic_eyes_ppm/classic_anime_blue.ppm b/data/classic_eyes_ppm/classic_anime_blue.ppm new file mode 100644 index 0000000..b53cdbc Binary files /dev/null and b/data/classic_eyes_ppm/classic_anime_blue.ppm differ diff --git a/data/classic_eyes_ppm/classic_anime_brown.ppm b/data/classic_eyes_ppm/classic_anime_brown.ppm new file mode 100644 index 0000000..f8a6081 Binary files /dev/null and b/data/classic_eyes_ppm/classic_anime_brown.ppm differ diff --git a/data/classic_eyes_ppm/classic_anime_green.ppm b/data/classic_eyes_ppm/classic_anime_green.ppm new file mode 100644 index 0000000..9ddd492 Binary files /dev/null and b/data/classic_eyes_ppm/classic_anime_green.ppm differ diff --git a/data/classic_eyes_ppm/classic_anime_grey.ppm b/data/classic_eyes_ppm/classic_anime_grey.ppm new file mode 100644 index 0000000..7d2fecd Binary files /dev/null and b/data/classic_eyes_ppm/classic_anime_grey.ppm differ diff --git a/data/classic_eyes_ppm/classic_anime_hazel.ppm b/data/classic_eyes_ppm/classic_anime_hazel.ppm new file mode 100644 index 0000000..1a7e407 Binary files /dev/null and b/data/classic_eyes_ppm/classic_anime_hazel.ppm differ diff --git a/data/classic_eyes_ppm/classic_cartoon_amber.ppm b/data/classic_eyes_ppm/classic_cartoon_amber.ppm new file mode 100644 index 0000000..d941767 Binary files /dev/null and b/data/classic_eyes_ppm/classic_cartoon_amber.ppm differ diff --git a/data/classic_eyes_ppm/classic_cartoon_blue.ppm b/data/classic_eyes_ppm/classic_cartoon_blue.ppm new file mode 100644 index 0000000..ad5d925 Binary files /dev/null and b/data/classic_eyes_ppm/classic_cartoon_blue.ppm differ diff --git a/data/classic_eyes_ppm/classic_cartoon_brown.ppm b/data/classic_eyes_ppm/classic_cartoon_brown.ppm new file mode 100644 index 0000000..281f8ad Binary files /dev/null and b/data/classic_eyes_ppm/classic_cartoon_brown.ppm differ diff --git a/data/classic_eyes_ppm/classic_cartoon_green.ppm b/data/classic_eyes_ppm/classic_cartoon_green.ppm new file mode 100644 index 0000000..42dc719 Binary files /dev/null and b/data/classic_eyes_ppm/classic_cartoon_green.ppm differ diff --git a/data/classic_eyes_ppm/classic_cartoon_grey.ppm b/data/classic_eyes_ppm/classic_cartoon_grey.ppm new file mode 100644 index 0000000..381e948 Binary files /dev/null and b/data/classic_eyes_ppm/classic_cartoon_grey.ppm differ diff --git a/data/classic_eyes_ppm/classic_cartoon_hazel.ppm b/data/classic_eyes_ppm/classic_cartoon_hazel.ppm new file mode 100644 index 0000000..c65809e Binary files /dev/null and b/data/classic_eyes_ppm/classic_cartoon_hazel.ppm differ diff --git a/data/classic_eyes_ppm/classic_realistic_amber.ppm b/data/classic_eyes_ppm/classic_realistic_amber.ppm new file mode 100644 index 0000000..8d983a9 Binary files /dev/null and b/data/classic_eyes_ppm/classic_realistic_amber.ppm differ diff --git a/data/classic_eyes_ppm/classic_realistic_blue.ppm b/data/classic_eyes_ppm/classic_realistic_blue.ppm new file mode 100644 index 0000000..961bb5a Binary files /dev/null and b/data/classic_eyes_ppm/classic_realistic_blue.ppm differ diff --git a/data/classic_eyes_ppm/classic_realistic_brown.ppm b/data/classic_eyes_ppm/classic_realistic_brown.ppm new file mode 100644 index 0000000..3a9c118 Binary files /dev/null and b/data/classic_eyes_ppm/classic_realistic_brown.ppm differ diff --git a/data/classic_eyes_ppm/classic_realistic_green.ppm b/data/classic_eyes_ppm/classic_realistic_green.ppm new file mode 100644 index 0000000..f10663f Binary files /dev/null and b/data/classic_eyes_ppm/classic_realistic_green.ppm differ diff --git a/data/classic_eyes_ppm/classic_realistic_grey.ppm b/data/classic_eyes_ppm/classic_realistic_grey.ppm new file mode 100644 index 0000000..b4c0744 Binary files /dev/null and b/data/classic_eyes_ppm/classic_realistic_grey.ppm differ diff --git a/data/classic_eyes_ppm/classic_realistic_hazel.ppm b/data/classic_eyes_ppm/classic_realistic_hazel.ppm new file mode 100644 index 0000000..2bf282e Binary files /dev/null and b/data/classic_eyes_ppm/classic_realistic_hazel.ppm differ diff --git a/data/classic_eyes_ppm/convert_to_png.py b/data/classic_eyes_ppm/convert_to_png.py new file mode 100644 index 0000000..49dbcde --- /dev/null +++ b/data/classic_eyes_ppm/convert_to_png.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +"""Convert PPM files to PNG using PIL.""" +from pathlib import Path +from PIL import Image + +ppm_dir = Path(__file__).parent +for ppm_file in ppm_dir.glob("*.ppm"): + png_file = ppm_file.with_suffix('.png') + img = Image.open(ppm_file) + img.save(png_file, 'PNG') + print(f"Converted: {ppm_file.name} -> {png_file.name}") diff --git a/data/classic_eyes_ppm/metadata.json b/data/classic_eyes_ppm/metadata.json new file mode 100644 index 0000000..253f699 --- /dev/null +++ b/data/classic_eyes_ppm/metadata.json @@ -0,0 +1,164 @@ +[ + { + "filename": "classic_realistic_blue.png", + "ppm_filename": "classic_realistic_blue.ppm", + "name": "Classic Realistic Eye - Blue", + "description": "A realistic style eye with blue iris color", + "tags": "eye,classic,realistic,blue,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_realistic_green.png", + "ppm_filename": "classic_realistic_green.ppm", + "name": "Classic Realistic Eye - Green", + "description": "A realistic style eye with green iris color", + "tags": "eye,classic,realistic,green,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_realistic_brown.png", + "ppm_filename": "classic_realistic_brown.ppm", + "name": "Classic Realistic Eye - Brown", + "description": "A realistic style eye with brown iris color", + "tags": "eye,classic,realistic,brown,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_realistic_hazel.png", + "ppm_filename": "classic_realistic_hazel.ppm", + "name": "Classic Realistic Eye - Hazel", + "description": "A realistic style eye with hazel iris color", + "tags": "eye,classic,realistic,hazel,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_realistic_grey.png", + "ppm_filename": "classic_realistic_grey.ppm", + "name": "Classic Realistic Eye - Grey", + "description": "A realistic style eye with grey iris color", + "tags": "eye,classic,realistic,grey,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_realistic_amber.png", + "ppm_filename": "classic_realistic_amber.ppm", + "name": "Classic Realistic Eye - Amber", + "description": "A realistic style eye with amber iris color", + "tags": "eye,classic,realistic,amber,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_anime_blue.png", + "ppm_filename": "classic_anime_blue.ppm", + "name": "Classic Anime Eye - Blue", + "description": "A anime style eye with blue iris color", + "tags": "eye,classic,anime,blue,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_anime_green.png", + "ppm_filename": "classic_anime_green.ppm", + "name": "Classic Anime Eye - Green", + "description": "A anime style eye with green iris color", + "tags": "eye,classic,anime,green,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_anime_brown.png", + "ppm_filename": "classic_anime_brown.ppm", + "name": "Classic Anime Eye - Brown", + "description": "A anime style eye with brown iris color", + "tags": "eye,classic,anime,brown,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_anime_hazel.png", + "ppm_filename": "classic_anime_hazel.ppm", + "name": "Classic Anime Eye - Hazel", + "description": "A anime style eye with hazel iris color", + "tags": "eye,classic,anime,hazel,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_anime_grey.png", + "ppm_filename": "classic_anime_grey.ppm", + "name": "Classic Anime Eye - Grey", + "description": "A anime style eye with grey iris color", + "tags": "eye,classic,anime,grey,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_anime_amber.png", + "ppm_filename": "classic_anime_amber.ppm", + "name": "Classic Anime Eye - Amber", + "description": "A anime style eye with amber iris color", + "tags": "eye,classic,anime,amber,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_cartoon_blue.png", + "ppm_filename": "classic_cartoon_blue.ppm", + "name": "Classic Cartoon Eye - Blue", + "description": "A cartoon style eye with blue iris color", + "tags": "eye,classic,cartoon,blue,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_cartoon_green.png", + "ppm_filename": "classic_cartoon_green.ppm", + "name": "Classic Cartoon Eye - Green", + "description": "A cartoon style eye with green iris color", + "tags": "eye,classic,cartoon,green,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_cartoon_brown.png", + "ppm_filename": "classic_cartoon_brown.ppm", + "name": "Classic Cartoon Eye - Brown", + "description": "A cartoon style eye with brown iris color", + "tags": "eye,classic,cartoon,brown,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_cartoon_hazel.png", + "ppm_filename": "classic_cartoon_hazel.ppm", + "name": "Classic Cartoon Eye - Hazel", + "description": "A cartoon style eye with hazel iris color", + "tags": "eye,classic,cartoon,hazel,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_cartoon_grey.png", + "ppm_filename": "classic_cartoon_grey.ppm", + "name": "Classic Cartoon Eye - Grey", + "description": "A cartoon style eye with grey iris color", + "tags": "eye,classic,cartoon,grey,medium", + "width": 200, + "height": 200 + }, + { + "filename": "classic_cartoon_amber.png", + "ppm_filename": "classic_cartoon_amber.ppm", + "name": "Classic Cartoon Eye - Amber", + "description": "A cartoon style eye with amber iris color", + "tags": "eye,classic,cartoon,amber,medium", + "width": 200, + "height": 200 + } +] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2d2bc89..ee51300 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: volumes: - ./data:/app/data - ./backend:/app + - ./scripts:/scripts environment: - DATABASE_URL=sqlite:///./data/ai_photo_edit.db - SECRET_KEY=${SECRET_KEY:-change-this-secret-key-in-production} diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 972b566..9560a70 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -1,6 +1,7 @@ import axios from 'axios'; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'; +// Use relative URLs to go through nginx proxy, or use env variable for direct connection +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; const api = axios.create({ baseURL: API_BASE_URL, diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..d0318d2 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,108 @@ +# Classic Eyes Scripts + +This directory contains scripts for generating and importing classic eye images into the AI Photo Edit patch library. + +## Overview + +The scripts generate a variety of classic eye styles that can be used as reusable patches for photo editing: + +- **Realistic eyes** - Detailed eyes with gradient irises and realistic highlights +- **Anime eyes** - Large, expressive eyes in anime style with prominent highlights +- **Cartoon eyes** - Simple, bold cartoon-style eyes + +Each style is available in multiple iris colors: blue, green, brown, hazel, grey, and amber. + +## Scripts + +### 1. `generate_classic_eyes_ppm.py` + +Generates classic eye images using only the Python standard library (no dependencies required). + +```bash +python scripts/generate_classic_eyes_ppm.py +``` + +This creates PPM format images in `data/classic_eyes_ppm/`. PPM is a simple image format that can be converted to PNG later. + +### 2. `download_classic_eyes.py` + +Full-featured script that generates eyes and imports them directly into the patch library. Requires PIL/Pillow. + +```bash +# Run inside the backend container +docker exec -it ai-photo-edit-backend python /app/../scripts/download_classic_eyes.py + +# Or with the API running +python scripts/download_classic_eyes.py --api --base-url http://localhost:8101 + +# Or save to a directory for later import +python scripts/download_classic_eyes.py --output-dir ./my_eyes +``` + +### 3. `startup_import_eyes.py` + +Converts PPM files to PNG and imports them into the patch library. Run this inside the backend container. + +```bash +docker exec -it ai-photo-edit-backend python /app/../scripts/startup_import_eyes.py +``` + +### 4. `import_saved_eyes.py` + +Import previously saved eye images from a directory. + +```bash +python scripts/import_saved_eyes.py /path/to/saved/eyes +``` + +## Quick Start + +1. **Generate the eye images** (no dependencies needed): + ```bash + python scripts/generate_classic_eyes_ppm.py + ``` + +2. **Start the application**: + ```bash + docker compose up -d + ``` + +3. **Import the eyes**: + ```bash + docker exec -it ai-photo-edit-backend python /scripts/startup_import_eyes.py + ``` + +4. **Verify in the app**: Open the patch library in the UI to see the imported classic eyes. + +## Generated Eyes + +| Style | Colors Available | Size | +|-----------|-------------------------------------------|--------| +| Realistic | Blue, Green, Brown, Hazel, Grey, Amber | 200x200| +| Anime | Blue, Green, Brown, Hazel, Grey, Amber | 200x200| +| Cartoon | Blue, Green, Brown, Hazel, Grey, Amber | 200x200| + +Total: 18 unique eye variants + +## Extending + +To add more eye styles or colors, edit the `generate_classic_eyes_ppm.py` or `download_classic_eyes.py` scripts: + +```python +# Add new color +colors["purple"] = (128, 0, 128) + +# Add new style in create_classic_eye() function +elif style == "fantasy": + # Your custom eye drawing code + pass +``` + +## File Formats + +- **PPM**: Portable Pixmap format - simple, universal, generated without dependencies +- **PNG**: Preferred format for the patch library - converted from PPM using PIL + +## License + +The generated eye images are created programmatically and are free to use without restrictions. diff --git a/scripts/download_classic_eyes.py b/scripts/download_classic_eyes.py new file mode 100644 index 0000000..87219a6 --- /dev/null +++ b/scripts/download_classic_eyes.py @@ -0,0 +1,629 @@ +#!/usr/bin/env python3 +""" +Download and import classic eye images into the patch library. + +This script downloads open-source/public domain eye images and imports them +into the AI Photo Edit patch library for use as reusable eye patches. + +Sources: +- OpenGameArt.org (CC0/Public Domain game assets) +- Generated stylized eyes using PIL +- Public domain vintage illustrations + +Usage: + # Run inside the backend container or with backend dependencies: + python scripts/download_classic_eyes.py + + # Or run via API when app is running: + python scripts/download_classic_eyes.py --api --base-url http://localhost:8101 +""" + +import os +import sys +import io +import json +import argparse +from pathlib import Path +from datetime import datetime + +# Try to import dependencies +try: + from PIL import Image, ImageDraw, ImageFilter + HAS_PIL = True +except ImportError: + HAS_PIL = False + print("Warning: PIL not available. Install with: pip install Pillow") + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / "backend")) + +try: + from sqlalchemy.orm import Session + from app.database import SessionLocal, engine, Base + from app.models.patch import Patch + from app.services.patch_library import PatchLibraryService + HAS_BACKEND = True +except ImportError: + HAS_BACKEND = False + print("Warning: Backend modules not available. Use --api mode or run inside backend container.") + + +# Public domain eye image sources (CC0/Public Domain) +CLASSIC_EYE_URLS = [ + # OpenGameArt style eyes - these are placeholder URLs + # In production, you would use actual public domain image URLs +] + +# We'll generate classic stylized eyes instead since downloading from external +# sources can be unreliable. These are better quality and guaranteed available. + + +def create_classic_eye( + size: tuple = (200, 200), + iris_color: tuple = (70, 130, 180), # Steel blue + pupil_size_ratio: float = 0.3, + iris_size_ratio: float = 0.7, + style: str = "realistic" +) -> Image.Image: + """ + Generate a classic stylized eye image. + + Args: + size: Output image size (width, height) + iris_color: RGB color for the iris + pupil_size_ratio: Ratio of pupil to iris + iris_size_ratio: Ratio of iris to eye + style: "realistic", "anime", "cartoon", "vintage" + + Returns: + PIL Image with transparent background + """ + img = Image.new('RGBA', size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + center_x, center_y = size[0] // 2, size[1] // 2 + eye_radius = min(size) // 2 - 5 + iris_radius = int(eye_radius * iris_size_ratio) + pupil_radius = int(iris_radius * pupil_size_ratio) + + if style == "realistic": + # White of the eye (sclera) with slight pink tint + sclera_color = (250, 245, 240, 255) + draw.ellipse( + [center_x - eye_radius, center_y - eye_radius, + center_x + eye_radius, center_y + eye_radius], + fill=sclera_color, + outline=(180, 160, 150, 255), + width=2 + ) + + # Iris with gradient effect + for i in range(iris_radius, 0, -2): + ratio = i / iris_radius + color = ( + int(iris_color[0] * ratio + 40 * (1 - ratio)), + int(iris_color[1] * ratio + 40 * (1 - ratio)), + int(iris_color[2] * ratio + 40 * (1 - ratio)), + 255 + ) + draw.ellipse( + [center_x - i, center_y - i, center_x + i, center_y + i], + fill=color + ) + + # Pupil + draw.ellipse( + [center_x - pupil_radius, center_y - pupil_radius, + center_x + pupil_radius, center_y + pupil_radius], + fill=(10, 10, 10, 255) + ) + + # Highlight/reflection + highlight_x = center_x - pupil_radius // 2 + highlight_y = center_y - pupil_radius // 2 + highlight_radius = pupil_radius // 3 + draw.ellipse( + [highlight_x - highlight_radius, highlight_y - highlight_radius, + highlight_x + highlight_radius, highlight_y + highlight_radius], + fill=(255, 255, 255, 200) + ) + + elif style == "anime": + # Large iris, small pupil, big highlight - anime style + iris_radius = int(eye_radius * 0.85) + + # Sclera + draw.ellipse( + [center_x - eye_radius, center_y - eye_radius, + center_x + eye_radius, center_y + eye_radius], + fill=(255, 255, 255, 255), + outline=(0, 0, 0, 255), + width=3 + ) + + # Large iris + draw.ellipse( + [center_x - iris_radius, center_y - iris_radius, + center_x + iris_radius, center_y + iris_radius], + fill=iris_color + (255,) + ) + + # Pupil + pupil_radius = int(iris_radius * 0.25) + draw.ellipse( + [center_x - pupil_radius, center_y - pupil_radius, + center_x + pupil_radius, center_y + pupil_radius], + fill=(0, 0, 0, 255) + ) + + # Large anime-style highlight + hl_x, hl_y = center_x - iris_radius // 3, center_y - iris_radius // 3 + hl_r = iris_radius // 3 + draw.ellipse( + [hl_x - hl_r, hl_y - hl_r, hl_x + hl_r, hl_y + hl_r], + fill=(255, 255, 255, 255) + ) + + # Secondary smaller highlight + hl2_x, hl2_y = center_x + iris_radius // 4, center_y + iris_radius // 4 + hl2_r = iris_radius // 6 + draw.ellipse( + [hl2_x - hl2_r, hl2_y - hl2_r, hl2_x + hl2_r, hl2_y + hl2_r], + fill=(255, 255, 255, 200) + ) + + elif style == "cartoon": + # Simple cartoon eye + # Sclera + draw.ellipse( + [center_x - eye_radius, center_y - eye_radius, + center_x + eye_radius, center_y + eye_radius], + fill=(255, 255, 255, 255), + outline=(0, 0, 0, 255), + width=4 + ) + + # Simple colored iris + draw.ellipse( + [center_x - iris_radius, center_y - iris_radius, + center_x + iris_radius, center_y + iris_radius], + fill=iris_color + (255,), + outline=(0, 0, 0, 255), + width=2 + ) + + # Pupil + draw.ellipse( + [center_x - pupil_radius, center_y - pupil_radius, + center_x + pupil_radius, center_y + pupil_radius], + fill=(0, 0, 0, 255) + ) + + # Highlight + hl_r = pupil_radius // 2 + draw.ellipse( + [center_x - pupil_radius - hl_r, center_y - pupil_radius - hl_r, + center_x - pupil_radius + hl_r, center_y - pupil_radius + hl_r], + fill=(255, 255, 255, 255) + ) + + elif style == "vintage": + # Vintage engraving style eye + # Multiple concentric circles for hatching effect + draw.ellipse( + [center_x - eye_radius, center_y - eye_radius, + center_x + eye_radius, center_y + eye_radius], + fill=(245, 235, 220, 255), + outline=(80, 60, 40, 255), + width=2 + ) + + # Iris with hatching-like rings + for i in range(iris_radius, pupil_radius, -4): + ratio = (i - pupil_radius) / (iris_radius - pupil_radius) + alpha = int(150 + 100 * ratio) + draw.ellipse( + [center_x - i, center_y - i, center_x + i, center_y + i], + outline=(60 + int(iris_color[0] * 0.3), + 50 + int(iris_color[1] * 0.3), + 40 + int(iris_color[2] * 0.3), alpha), + width=1 + ) + + # Dark pupil + draw.ellipse( + [center_x - pupil_radius, center_y - pupil_radius, + center_x + pupil_radius, center_y + pupil_radius], + fill=(20, 15, 10, 255) + ) + + # Apply slight blur for more natural look + if style in ["realistic", "vintage"]: + img = img.filter(ImageFilter.GaussianBlur(radius=0.5)) + + return img + + +def generate_eye_variants() -> list: + """ + Generate a set of classic eye variants with different colors and styles. + + Returns: + List of (name, description, tags, image) tuples + """ + variants = [] + + # Eye colors + colors = { + "blue": (70, 130, 180), + "green": (60, 140, 90), + "brown": (139, 90, 43), + "hazel": (150, 120, 70), + "grey": (120, 130, 140), + "amber": (180, 130, 50), + "violet": (138, 43, 226), + "black": (30, 30, 35), + } + + # Styles + styles = ["realistic", "anime", "cartoon", "vintage"] + + # Sizes + sizes = { + "small": (100, 100), + "medium": (200, 200), + "large": (300, 300), + } + + # Generate all combinations for medium size, main styles + for style in styles: + for color_name, color_rgb in colors.items(): + size = sizes["medium"] + img = create_classic_eye( + size=size, + iris_color=color_rgb, + style=style + ) + + name = f"Classic {style.title()} Eye - {color_name.title()}" + description = f"A {style} style eye with {color_name} iris color" + tags = f"eye,classic,{style},{color_name},medium" + + variants.append((name, description, tags, img)) + + # Add some extra size variants for most popular combinations + popular = [ + ("blue", "realistic"), + ("brown", "realistic"), + ("green", "realistic"), + ("blue", "anime"), + ("green", "anime"), + ] + + for color_name, style in popular: + color_rgb = colors[color_name] + for size_name, size in sizes.items(): + if size_name == "medium": + continue # Already generated + + img = create_classic_eye( + size=size, + iris_color=color_rgb, + style=style + ) + + name = f"Classic {style.title()} Eye - {color_name.title()} ({size_name})" + description = f"A {size_name} {style} style eye with {color_name} iris" + tags = f"eye,classic,{style},{color_name},{size_name}" + + variants.append((name, description, tags, img)) + + return variants + + +def download_external_eyes() -> list: + """ + Download eye images from external public domain sources. + + Returns: + List of (name, description, tags, image) tuples + """ + if not HAS_REQUESTS or not HAS_PIL: + print(" Skipping external downloads (missing dependencies)") + return [] + + results = [] + + # OpenGameArt and other CC0 sources + # These are example URLs - in production, curate actual public domain images + external_sources = [ + { + "url": "https://opengameart.org/sites/default/files/eye_0.png", + "name": "OpenGameArt Eye Sprite", + "description": "Pixel art style eye from OpenGameArt (CC0)", + "tags": "eye,pixel,game,sprite,public_domain" + }, + ] + + for source in external_sources: + try: + response = requests.get(source["url"], timeout=10) + if response.status_code == 200: + img = Image.open(io.BytesIO(response.content)).convert('RGBA') + results.append(( + source["name"], + source["description"], + source["tags"], + img + )) + print(f" Downloaded: {source['name']}") + else: + print(f" Failed to download {source['name']}: HTTP {response.status_code}") + except Exception as e: + print(f" Error downloading {source['name']}: {e}") + + return results + + +def import_eyes_to_library(eyes: list, db: Session, patch_service: PatchLibraryService): + """ + Import eye images into the patch library database. + + Args: + eyes: List of (name, description, tags, image) tuples + db: Database session + patch_service: PatchLibraryService instance + """ + imported_count = 0 + + for name, description, tags, img in eyes: + try: + # Check if patch with same name already exists + existing = db.query(Patch).filter(Patch.name == name).first() + if existing: + print(f" Skipping (exists): {name}") + continue + + # Create database record first to get ID + patch = Patch( + name=name, + description=description, + source_type="imported", + width=img.width, + height=img.height, + tags=tags, + category="eye", + is_public=True, + file_path="", # Will update after saving + thumbnail_path="" + ) + db.add(patch) + db.flush() # Get the ID + + # Save image file + patch_path = patch_service.get_patch_path(patch.id) + img.save(patch_path, 'PNG') + + # Create thumbnail + thumb_path = patch_service.get_thumbnail_path(patch.id) + patch_service.create_thumbnail(patch_path, thumb_path) + + # Update paths in database + patch.file_path = f"patch_library/{patch.id}.png" + patch.thumbnail_path = f"patch_library/{patch.id}_thumb.png" + + db.commit() + imported_count += 1 + print(f" Imported: {name} (ID: {patch.id})") + + except Exception as e: + db.rollback() + print(f" Error importing {name}: {e}") + + return imported_count + + +def import_via_api(eyes: list, base_url: str) -> int: + """ + Import eyes via the REST API. + + Args: + eyes: List of (name, description, tags, image) tuples + base_url: Base URL of the API (e.g., http://localhost:8101) + + Returns: + Number of successfully imported eyes + """ + if not HAS_REQUESTS: + print("Error: requests library required for API mode") + return 0 + + imported = 0 + for name, description, tags, img in eyes: + try: + # Convert image to bytes + img_buffer = io.BytesIO() + img.save(img_buffer, format='PNG') + img_buffer.seek(0) + + # Upload via API + files = {'file': (f'{name}.png', img_buffer, 'image/png')} + data = { + 'name': name, + 'description': description, + 'tags': tags, + 'category': 'eye', + 'source_type': 'imported' + } + + response = requests.post( + f"{base_url}/patches/", + files=files, + data=data, + timeout=30 + ) + + if response.status_code in [200, 201]: + patch_id = response.json().get('id', 'unknown') + print(f" Imported: {name} (ID: {patch_id})") + imported += 1 + elif response.status_code == 409: + print(f" Skipping (exists): {name}") + else: + print(f" Failed to import {name}: HTTP {response.status_code}") + + except Exception as e: + print(f" Error importing {name}: {e}") + + return imported + + +def save_eyes_to_files(eyes: list, output_dir: Path) -> int: + """ + Save generated eyes as PNG files for manual import later. + + Args: + eyes: List of (name, description, tags, image) tuples + output_dir: Directory to save images + + Returns: + Number of saved files + """ + output_dir.mkdir(parents=True, exist_ok=True) + saved = 0 + + # Create a metadata file + metadata = [] + + for name, description, tags, img in eyes: + try: + # Create safe filename + safe_name = name.replace(' ', '_').replace('-', '_').lower() + safe_name = ''.join(c for c in safe_name if c.isalnum() or c == '_') + filename = f"{safe_name}.png" + + filepath = output_dir / filename + img.save(filepath, 'PNG') + + metadata.append({ + 'filename': filename, + 'name': name, + 'description': description, + 'tags': tags, + 'width': img.width, + 'height': img.height + }) + + saved += 1 + + except Exception as e: + print(f" Error saving {name}: {e}") + + # Write metadata JSON + metadata_path = output_dir / "metadata.json" + with open(metadata_path, 'w') as f: + json.dump(metadata, f, indent=2) + + print(f" Saved metadata to: {metadata_path}") + + return saved + + +def main(): + """Main function to download and import classic eyes.""" + parser = argparse.ArgumentParser(description='Download and import classic eye images') + parser.add_argument('--api', action='store_true', help='Use API mode to import') + parser.add_argument('--base-url', default='http://localhost:8101', help='API base URL') + parser.add_argument('--output-dir', help='Save images to directory instead of importing') + parser.add_argument('--skip-external', action='store_true', help='Skip downloading external images') + args = parser.parse_args() + + print("=" * 60) + print("Classic Eye Importer for AI Photo Edit") + print("=" * 60) + + if not HAS_PIL: + print("\nError: PIL/Pillow is required. Install with: pip install Pillow") + print("Or run this script inside the backend container.") + sys.exit(1) + + all_eyes = [] + + # Generate stylized eyes + print("\n[1/3] Generating classic stylized eyes...") + generated_eyes = generate_eye_variants() + print(f" Generated {len(generated_eyes)} eye variants") + all_eyes.extend(generated_eyes) + + # Download external public domain eyes + if not args.skip_external: + print("\n[2/3] Downloading external public domain eyes...") + external_eyes = download_external_eyes() + print(f" Downloaded {len(external_eyes)} external eyes") + all_eyes.extend(external_eyes) + else: + print("\n[2/3] Skipping external downloads (--skip-external)") + + # Determine import method + print(f"\n[3/3] Processing {len(all_eyes)} eyes...") + + if args.output_dir: + # Save to files + output_dir = Path(args.output_dir) + print(f" Saving to directory: {output_dir}") + saved = save_eyes_to_files(all_eyes, output_dir) + print(f" Saved {saved} eye images to {output_dir}") + + elif args.api: + # Import via API + print(f" Using API mode: {args.base_url}") + imported = import_via_api(all_eyes, args.base_url) + print(f"\n Successfully imported: {imported}") + print(f" Skipped/Failed: {len(all_eyes) - imported}") + + elif HAS_BACKEND: + # Direct database import + print(" Using direct database import...") + Base.metadata.create_all(bind=engine) + db = SessionLocal() + + data_dir = os.environ.get("DATA_DIR", str(Path(__file__).parent.parent / "data")) + patch_service = PatchLibraryService(data_dir) + print(f" Patch library dir: {patch_service.patch_library_dir}") + + imported = import_eyes_to_library(all_eyes, db, patch_service) + + # Summary + print("\n" + "=" * 60) + print("Import Complete!") + print(f" Total eyes processed: {len(all_eyes)}") + print(f" Successfully imported: {imported}") + print(f" Skipped (duplicates): {len(all_eyes) - imported}") + print("=" * 60) + + # Show sample of imported eyes + print("\nSample of imported eyes:") + samples = db.query(Patch).filter(Patch.category == "eye").limit(5).all() + for p in samples: + print(f" - {p.name} ({p.width}x{p.height}) [ID: {p.id}]") + + db.close() + + else: + # Fallback: save to files + output_dir = Path(__file__).parent.parent / "data" / "classic_eyes_import" + print(f" Backend not available. Saving to: {output_dir}") + saved = save_eyes_to_files(all_eyes, output_dir) + print(f"\n Saved {saved} eye images") + print(f"\n To import later, run inside backend container:") + print(f" python scripts/import_saved_eyes.py {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_classic_eyes_ppm.py b/scripts/generate_classic_eyes_ppm.py new file mode 100644 index 0000000..c76e010 --- /dev/null +++ b/scripts/generate_classic_eyes_ppm.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python3 +""" +Generate classic eye images using only the Python standard library. +Creates PPM format images that can be converted to PNG when PIL is available. + +This script requires NO external dependencies - it uses the PPM image format +which is a simple text/binary format readable by most image tools. + +Usage: + python scripts/generate_classic_eyes_ppm.py + +The generated PPM files can be converted to PNG using: + - PIL/Pillow: Image.open('eye.ppm').save('eye.png') + - ImageMagick: convert eye.ppm eye.png + - GIMP: Open and export as PNG +""" + +import os +import math +from pathlib import Path + + +def create_ppm(width: int, height: int) -> list: + """Create an empty PPM image as a 2D list of (R, G, B) tuples.""" + return [[(0, 0, 0) for _ in range(width)] for _ in range(height)] + + +def set_pixel(img: list, x: int, y: int, color: tuple): + """Set a pixel in the image, with bounds checking.""" + height = len(img) + width = len(img[0]) if height > 0 else 0 + if 0 <= x < width and 0 <= y < height: + img[y][x] = color + + +def draw_filled_circle(img: list, cx: int, cy: int, radius: int, color: tuple): + """Draw a filled circle.""" + for y in range(-radius, radius + 1): + for x in range(-radius, radius + 1): + if x*x + y*y <= radius*radius: + set_pixel(img, cx + x, cy + y, color) + + +def draw_circle_ring(img: list, cx: int, cy: int, radius: int, color: tuple, thickness: int = 1): + """Draw a circle outline.""" + for y in range(-radius - thickness, radius + thickness + 1): + for x in range(-radius - thickness, radius + thickness + 1): + dist_sq = x*x + y*y + if (radius - thickness)**2 <= dist_sq <= (radius + thickness)**2: + set_pixel(img, cx + x, cy + y, color) + + +def blend_color(c1: tuple, c2: tuple, ratio: float) -> tuple: + """Blend two colors. ratio=0 gives c1, ratio=1 gives c2.""" + return ( + int(c1[0] * (1 - ratio) + c2[0] * ratio), + int(c1[1] * (1 - ratio) + c2[1] * ratio), + int(c1[2] * (1 - ratio) + c2[2] * ratio) + ) + + +def save_ppm(img: list, filepath: str): + """Save image as PPM format (P6 binary).""" + height = len(img) + width = len(img[0]) if height > 0 else 0 + + with open(filepath, 'wb') as f: + # PPM header + f.write(f"P6\n{width} {height}\n255\n".encode()) + # Pixel data + for row in img: + for r, g, b in row: + f.write(bytes([r, g, b])) + + +def save_ppm_text(img: list, filepath: str): + """Save image as PPM format (P3 text - more portable).""" + height = len(img) + width = len(img[0]) if height > 0 else 0 + + with open(filepath, 'w') as f: + f.write(f"P3\n{width} {height}\n255\n") + for row in img: + line = ' '.join(f"{r} {g} {b}" for r, g, b in row) + f.write(line + '\n') + + +def create_classic_eye( + size: int = 200, + iris_color: tuple = (70, 130, 180), + style: str = "realistic" +) -> list: + """ + Generate a classic stylized eye image. + + Args: + size: Image size (square) + iris_color: RGB color for the iris + style: "realistic", "anime", "cartoon" + + Returns: + 2D list of (R, G, B) tuples + """ + img = create_ppm(size, size) + + cx, cy = size // 2, size // 2 + eye_radius = size // 2 - 5 + iris_radius = int(eye_radius * 0.7) + pupil_radius = int(iris_radius * 0.35) + + if style == "realistic": + # White of the eye (sclera) + sclera_color = (250, 245, 240) + draw_filled_circle(img, cx, cy, eye_radius, sclera_color) + + # Sclera outline + draw_circle_ring(img, cx, cy, eye_radius, (180, 160, 150), 2) + + # Iris with gradient effect (simplified) + for r in range(iris_radius, 0, -1): + ratio = r / iris_radius + color = blend_color((40, 40, 40), iris_color, ratio) + draw_circle_ring(img, cx, cy, r, color, 1) + + # Pupil + draw_filled_circle(img, cx, cy, pupil_radius, (10, 10, 10)) + + # Highlight + hl_x = cx - pupil_radius // 2 + hl_y = cy - pupil_radius // 2 + hl_r = pupil_radius // 3 + draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255)) + + elif style == "anime": + # Larger iris for anime style + iris_radius = int(eye_radius * 0.85) + + # Sclera + draw_filled_circle(img, cx, cy, eye_radius, (255, 255, 255)) + draw_circle_ring(img, cx, cy, eye_radius, (0, 0, 0), 3) + + # Large iris + draw_filled_circle(img, cx, cy, iris_radius, iris_color) + + # Pupil + pupil_radius = int(iris_radius * 0.25) + draw_filled_circle(img, cx, cy, pupil_radius, (0, 0, 0)) + + # Large highlight + hl_x = cx - iris_radius // 3 + hl_y = cy - iris_radius // 3 + hl_r = iris_radius // 3 + draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255)) + + # Secondary highlight + hl2_x = cx + iris_radius // 4 + hl2_y = cy + iris_radius // 4 + hl2_r = iris_radius // 6 + draw_filled_circle(img, hl2_x, hl2_y, hl2_r, (220, 220, 220)) + + elif style == "cartoon": + # Simple cartoon eye + draw_filled_circle(img, cx, cy, eye_radius, (255, 255, 255)) + draw_circle_ring(img, cx, cy, eye_radius, (0, 0, 0), 4) + + draw_filled_circle(img, cx, cy, iris_radius, iris_color) + draw_circle_ring(img, cx, cy, iris_radius, (0, 0, 0), 2) + + draw_filled_circle(img, cx, cy, pupil_radius, (0, 0, 0)) + + # Highlight + hl_x = cx - pupil_radius + hl_y = cy - pupil_radius + hl_r = pupil_radius // 2 + draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255)) + + return img + + +def main(): + """Generate a set of classic eye images.""" + output_dir = Path(__file__).parent.parent / "data" / "classic_eyes_ppm" + output_dir.mkdir(parents=True, exist_ok=True) + + print("=" * 60) + print("Classic Eye Generator (PPM Format)") + print("=" * 60) + print(f"Output directory: {output_dir}") + + # Eye colors + colors = { + "blue": (70, 130, 180), + "green": (60, 140, 90), + "brown": (139, 90, 43), + "hazel": (150, 120, 70), + "grey": (120, 130, 140), + "amber": (180, 130, 50), + } + + styles = ["realistic", "anime", "cartoon"] + + metadata = [] + generated = 0 + + print("\nGenerating eyes...") + + for style in styles: + for color_name, color_rgb in colors.items(): + name = f"classic_{style}_{color_name}" + filename = f"{name}.ppm" + filepath = output_dir / filename + + img = create_classic_eye(size=200, iris_color=color_rgb, style=style) + save_ppm(img, str(filepath)) + + metadata.append({ + 'filename': filename.replace('.ppm', '.png'), # For after conversion + 'ppm_filename': filename, + 'name': f"Classic {style.title()} Eye - {color_name.title()}", + 'description': f"A {style} style eye with {color_name} iris color", + 'tags': f"eye,classic,{style},{color_name},medium", + 'width': 200, + 'height': 200 + }) + + generated += 1 + print(f" Generated: {name}.ppm") + + # Save metadata + import json + metadata_path = output_dir / "metadata.json" + with open(metadata_path, 'w') as f: + json.dump(metadata, f, indent=2) + + print(f"\n Metadata saved to: {metadata_path}") + print(f"\nGenerated {generated} eye images in PPM format.") + + # Create conversion script + convert_script = output_dir / "convert_to_png.py" + with open(convert_script, 'w') as f: + f.write('''#!/usr/bin/env python3 +"""Convert PPM files to PNG using PIL.""" +from pathlib import Path +from PIL import Image + +ppm_dir = Path(__file__).parent +for ppm_file in ppm_dir.glob("*.ppm"): + png_file = ppm_file.with_suffix('.png') + img = Image.open(ppm_file) + img.save(png_file, 'PNG') + print(f"Converted: {ppm_file.name} -> {png_file.name}") +''') + + print(f"\n Conversion script: {convert_script}") + print("\nTo convert PPM to PNG, run inside the backend container:") + print(f" python {convert_script}") + print("\nOr use ImageMagick:") + print(f" cd {output_dir} && for f in *.ppm; do convert \"$f\" \"${{f%.ppm}}.png\"; done") + + +if __name__ == "__main__": + main() diff --git a/scripts/import_saved_eyes.py b/scripts/import_saved_eyes.py new file mode 100644 index 0000000..b25455c --- /dev/null +++ b/scripts/import_saved_eyes.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Import previously saved eye images into the patch library. + +This script reads eye images from a directory (along with metadata.json) +and imports them into the patch library database. + +Usage: + python scripts/import_saved_eyes.py /path/to/saved/eyes + +This script must be run with backend dependencies available +(e.g., inside the backend container). +""" + +import os +import sys +import json +import argparse +from pathlib import Path + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / "backend")) + +from PIL import Image +from sqlalchemy.orm import Session +from app.database import SessionLocal, engine, Base +from app.models.patch import Patch +from app.services.patch_library import PatchLibraryService + + +def import_from_directory(source_dir: Path, db: Session, patch_service: PatchLibraryService) -> int: + """ + Import eye images from a directory. + + Args: + source_dir: Directory containing eye images and metadata.json + db: Database session + patch_service: PatchLibraryService instance + + Returns: + Number of successfully imported images + """ + metadata_path = source_dir / "metadata.json" + + if not metadata_path.exists(): + print(f"Error: metadata.json not found in {source_dir}") + print("Scanning for PNG files instead...") + + # Fallback: import all PNG files with default metadata + png_files = list(source_dir.glob("*.png")) + metadata = [] + for png_file in png_files: + img = Image.open(png_file) + metadata.append({ + 'filename': png_file.name, + 'name': png_file.stem.replace('_', ' ').title(), + 'description': f'Imported eye image: {png_file.name}', + 'tags': 'eye,imported', + 'width': img.width, + 'height': img.height + }) + else: + with open(metadata_path, 'r') as f: + metadata = json.load(f) + + imported = 0 + + for item in metadata: + try: + filename = item['filename'] + filepath = source_dir / filename + + if not filepath.exists(): + print(f" Skipping (file not found): {filename}") + continue + + name = item['name'] + description = item.get('description', '') + tags = item.get('tags', 'eye,imported') + + # Check if already exists + existing = db.query(Patch).filter(Patch.name == name).first() + if existing: + print(f" Skipping (exists): {name}") + continue + + # Load image + img = Image.open(filepath) + + # Create database record + patch = Patch( + name=name, + description=description, + source_type="imported", + width=img.width, + height=img.height, + tags=tags, + category="eye", + is_public=True, + file_path="", + thumbnail_path="" + ) + db.add(patch) + db.flush() + + # Save to patch library + patch_path = patch_service.get_patch_path(patch.id) + img.save(patch_path, 'PNG') + + # Create thumbnail + thumb_path = patch_service.get_thumbnail_path(patch.id) + patch_service.create_thumbnail(patch_path, thumb_path) + + # Update paths + patch.file_path = f"patch_library/{patch.id}.png" + patch.thumbnail_path = f"patch_library/{patch.id}_thumb.png" + + db.commit() + imported += 1 + print(f" Imported: {name} (ID: {patch.id})") + + except Exception as e: + db.rollback() + print(f" Error importing {item.get('filename', 'unknown')}: {e}") + + return imported + + +def main(): + parser = argparse.ArgumentParser(description='Import saved eye images to patch library') + parser.add_argument('source_dir', help='Directory containing eye images and metadata.json') + args = parser.parse_args() + + source_dir = Path(args.source_dir) + if not source_dir.exists(): + print(f"Error: Directory not found: {source_dir}") + sys.exit(1) + + print("=" * 60) + print("Eye Image Importer") + print("=" * 60) + + # Initialize database + print("\nInitializing database...") + Base.metadata.create_all(bind=engine) + db = SessionLocal() + + # Initialize patch library + data_dir = os.environ.get("DATA_DIR", str(Path(__file__).parent.parent / "data")) + patch_service = PatchLibraryService(data_dir) + + print(f"Source directory: {source_dir}") + print(f"Patch library: {patch_service.patch_library_dir}") + + # Import + print("\nImporting eyes...") + imported = import_from_directory(source_dir, db, patch_service) + + print("\n" + "=" * 60) + print(f"Import Complete! Imported {imported} eyes.") + print("=" * 60) + + db.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/startup_import_eyes.py b/scripts/startup_import_eyes.py new file mode 100644 index 0000000..da149bc --- /dev/null +++ b/scripts/startup_import_eyes.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +Startup script to import classic eyes into the patch library. +This should be run once when the backend starts, or manually. + +Usage: + python scripts/startup_import_eyes.py + +This script: +1. Converts PPM files to PNG (if needed) +2. Imports all classic eyes into the patch library database +""" + +import os +import sys +import json +from pathlib import Path + +# Add parent directory to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent / "backend")) + +try: + from PIL import Image + from sqlalchemy.orm import Session + from app.database import SessionLocal, engine, Base + from app.models.patch import Patch + from app.services.patch_library import PatchLibraryService +except ImportError as e: + print(f"Error: Required modules not available: {e}") + print("Run this script inside the backend container.") + sys.exit(1) + + +def convert_ppm_to_png(ppm_dir: Path) -> int: + """Convert all PPM files to PNG in the given directory.""" + converted = 0 + for ppm_file in ppm_dir.glob("*.ppm"): + png_file = ppm_file.with_suffix('.png') + if not png_file.exists(): + try: + img = Image.open(ppm_file) + img.save(png_file, 'PNG') + converted += 1 + print(f" Converted: {ppm_file.name} -> {png_file.name}") + except Exception as e: + print(f" Error converting {ppm_file.name}: {e}") + return converted + + +def import_eyes(source_dir: Path, db: Session, patch_service: PatchLibraryService) -> int: + """Import eye images from the given directory.""" + metadata_path = source_dir / "metadata.json" + + if not metadata_path.exists(): + print(f" No metadata.json found in {source_dir}") + return 0 + + with open(metadata_path, 'r') as f: + metadata = json.load(f) + + imported = 0 + + for item in metadata: + try: + # Try PNG first, then PPM + filename = item.get('filename', item.get('ppm_filename', '')).replace('.ppm', '.png') + filepath = source_dir / filename + + if not filepath.exists(): + # Try PPM + filepath = source_dir / item.get('ppm_filename', filename.replace('.png', '.ppm')) + + if not filepath.exists(): + continue + + name = item['name'] + + # Check if already exists + existing = db.query(Patch).filter(Patch.name == name).first() + if existing: + continue # Silent skip for duplicates on startup + + # Load and convert if PPM + img = Image.open(filepath).convert('RGBA') + + # Create database record + patch = Patch( + name=name, + description=item.get('description', ''), + source_type="imported", + width=img.width, + height=img.height, + tags=item.get('tags', 'eye,classic'), + category="eye", + is_public=True, + file_path="", + thumbnail_path="" + ) + db.add(patch) + db.flush() + + # Save to patch library as PNG + patch_path = patch_service.get_patch_path(patch.id) + img.save(patch_path, 'PNG') + + # Create thumbnail + thumb_path = patch_service.get_thumbnail_path(patch.id) + patch_service.create_thumbnail(patch_path, thumb_path) + + # Update paths + patch.file_path = f"patch_library/{patch.id}.png" + patch.thumbnail_path = f"patch_library/{patch.id}_thumb.png" + + db.commit() + imported += 1 + print(f" Imported: {name} (ID: {patch.id})") + + except Exception as e: + db.rollback() + print(f" Error: {e}") + + return imported + + +def main(): + print("=" * 60) + print("Classic Eyes Startup Import") + print("=" * 60) + + # Find the classic eyes directory + data_dir = Path(os.environ.get("DATA_DIR", "/app/data")) + if not data_dir.exists(): + data_dir = Path(__file__).parent.parent / "data" + + ppm_dir = data_dir / "classic_eyes_ppm" + + if not ppm_dir.exists(): + print(f"\nNo classic eyes found at: {ppm_dir}") + print("Run generate_classic_eyes_ppm.py first.") + return + + # Initialize database + print("\nInitializing database...") + Base.metadata.create_all(bind=engine) + db = SessionLocal() + + # Initialize patch library + patch_service = PatchLibraryService(str(data_dir)) + + # Convert PPM to PNG + print("\nConverting PPM to PNG (if needed)...") + converted = convert_ppm_to_png(ppm_dir) + if converted > 0: + print(f" Converted {converted} files") + else: + print(" All files already converted") + + # Import eyes + print("\nImporting classic eyes...") + imported = import_eyes(ppm_dir, db, patch_service) + + # Count existing + total = db.query(Patch).filter(Patch.category == "eye").count() + + print("\n" + "=" * 60) + print(f"Import Complete!") + print(f" Newly imported: {imported}") + print(f" Total eye patches in library: {total}") + print("=" * 60) + + db.close() + + +if __name__ == "__main__": + main()