Merge pull request #7 from outis1one/claude/download-classic-eyes-EWMFj

Claude/download classic eyes ewm fj
This commit is contained in:
outis1one
2026-01-24 21:59:16 -05:00
committed by GitHub
27 changed files with 1519 additions and 1 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+11
View File
@@ -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}")
+164
View File
@@ -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
}
]
+1
View File
@@ -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}
+2 -1
View File
@@ -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,
+108
View File
@@ -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.
+629
View File
@@ -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()
+262
View File
@@ -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()
+167
View File
@@ -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()
+175
View File
@@ -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()