Merge pull request #7 from outis1one/claude/download-classic-eyes-EWMFj
Claude/download classic eyes ewm fj
This commit is contained in:
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.
@@ -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}")
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user