paintplus: vendor the app source and rename from EditmaskwithAI
Bring the full EditmaskwithAI application into the repo under paintplus/ (429 files) so the service is self-contained — the installer copies the vendored source to ~/docker/paintplus/src instead of cloning at runtime. Rename to PaintPlus (service + branding; app logic untouched): - services/editmaskwithai.sh -> services/paintplus.sh (register_service paintplus, install_paintplus, ~/docker/paintplus, Caddy paintplus:8000, Authelia option preserved) - container names -> paintplus across docker-compose*.yml; dev network -> paintplus-network - browser <title> -> "PaintPlus - AI Image Editor"; README heading -> PaintPlus with upstream provenance note - README utilities table: editmaskwithai -> paintplus Backend/frontend code (help strings referencing the old container name, the ai_photo_edit.db filename) is intentionally left as-is to avoid touching application logic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
This commit is contained in:
@@ -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,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download Real-ESRGAN NCNN Vulkan binary.
|
||||
|
||||
This gives you fast AI upscaling on ANY GPU (Intel/AMD/NVIDIA integrated or discrete,
|
||||
Apple Metal) without needing CUDA or Python AI packages.
|
||||
|
||||
Usage:
|
||||
docker exec -it ai-photo-edit python /scripts/download_realesrgan.py
|
||||
# or locally:
|
||||
python scripts/download_realesrgan.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import zipfile
|
||||
import urllib.request
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
DEST_DIR = Path("/app/data/models/realesrgan")
|
||||
VERSION = "v0.2.5.0"
|
||||
|
||||
PLATFORM_MAP = {
|
||||
"linux": f"realesrgan-ncnn-vulkan-{VERSION}-ubuntu.zip",
|
||||
"darwin": f"realesrgan-ncnn-vulkan-{VERSION}-macos.zip",
|
||||
"win32": f"realesrgan-ncnn-vulkan-{VERSION}-windows.zip",
|
||||
"windows": f"realesrgan-ncnn-vulkan-{VERSION}-windows.zip",
|
||||
}
|
||||
|
||||
BASE_URL = f"https://github.com/xinntao/Real-ESRGAN/releases/download/{VERSION}"
|
||||
|
||||
|
||||
def main():
|
||||
plat = sys.platform.lower()
|
||||
if plat not in PLATFORM_MAP:
|
||||
print(f"Unknown platform: {plat}")
|
||||
sys.exit(1)
|
||||
|
||||
filename = PLATFORM_MAP[plat]
|
||||
url = f"{BASE_URL}/{filename}"
|
||||
zip_path = DEST_DIR / filename
|
||||
|
||||
DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
binary_name = "realesrgan-ncnn-vulkan.exe" if "win" in plat else "realesrgan-ncnn-vulkan"
|
||||
binary_path = DEST_DIR / binary_name
|
||||
|
||||
if binary_path.exists():
|
||||
print(f"Already installed: {binary_path}")
|
||||
print("Delete it and re-run to reinstall.")
|
||||
return
|
||||
|
||||
print(f"Downloading Real-ESRGAN NCNN Vulkan {VERSION} for {plat}...")
|
||||
print(f"URL: {url}")
|
||||
|
||||
def progress(count, block_size, total_size):
|
||||
if total_size > 0 and count % 100 == 0:
|
||||
pct = min(100, count * block_size * 100 // total_size)
|
||||
mb = count * block_size / 1024 / 1024
|
||||
total_mb = total_size / 1024 / 1024
|
||||
print(f" {pct}% ({mb:.1f}/{total_mb:.1f} MB)", end="\r")
|
||||
|
||||
urllib.request.urlretrieve(url, zip_path, progress)
|
||||
print(f"\nDownloaded to {zip_path}")
|
||||
|
||||
print("Extracting...")
|
||||
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||
zf.extractall(DEST_DIR)
|
||||
|
||||
# The zip extracts into a subdirectory — find the binary
|
||||
found = list(DEST_DIR.rglob(binary_name))
|
||||
if not found:
|
||||
print(f"ERROR: Could not find {binary_name} in extracted files.")
|
||||
sys.exit(1)
|
||||
|
||||
extracted = found[0]
|
||||
if extracted != binary_path:
|
||||
extracted.rename(binary_path)
|
||||
|
||||
# Make executable on unix
|
||||
if "win" not in plat:
|
||||
binary_path.chmod(binary_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
||||
|
||||
# Clean up zip
|
||||
zip_path.unlink(missing_ok=True)
|
||||
|
||||
print(f"\nInstalled: {binary_path}")
|
||||
print("\nTest it:")
|
||||
print(f" {binary_path} --help")
|
||||
print("\nThe upscaler will auto-detect this binary next time you use Upscale in PaintPlus.")
|
||||
print("Restart the backend container to clear the capability cache:")
|
||||
print(" docker-compose restart backend")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download SAM (Segment Anything Model) for local inference.
|
||||
|
||||
This script downloads the SAM model checkpoint to a persistent directory
|
||||
so it survives container rebuilds.
|
||||
|
||||
Models available:
|
||||
- sam_vit_b: ~375MB (default, good balance of speed/quality)
|
||||
- sam_vit_l: ~1.2GB (better quality, slower)
|
||||
- sam_vit_h: ~2.5GB (best quality, slowest)
|
||||
|
||||
Usage:
|
||||
python scripts/download_sam_model.py [model_type]
|
||||
|
||||
model_type: vit_b (default), vit_l, or vit_h
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
# Model URLs from Meta's official releases
|
||||
SAM_MODELS = {
|
||||
'vit_b': {
|
||||
'url': 'https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth',
|
||||
'filename': 'sam_vit_b_01ec64.pth',
|
||||
'size': '375MB'
|
||||
},
|
||||
'vit_l': {
|
||||
'url': 'https://dl.fbaipublicfiles.com/segment_anything/sam_vit_l_0b3195.pth',
|
||||
'filename': 'sam_vit_l_0b3195.pth',
|
||||
'size': '1.2GB'
|
||||
},
|
||||
'vit_h': {
|
||||
'url': 'https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth',
|
||||
'filename': 'sam_vit_h_4b8939.pth',
|
||||
'size': '2.5GB'
|
||||
}
|
||||
}
|
||||
|
||||
def create_symlink(symlink_path: Path, target_name: str):
|
||||
"""Best-effort convenience symlink. Never raises — a missing/stale
|
||||
symlink is harmless (callers also check the real filename directly),
|
||||
but data/models/ is often root-owned from a prior Docker run, which
|
||||
makes unlink/symlink_to fail with PermissionError for other users."""
|
||||
try:
|
||||
if symlink_path.exists() or symlink_path.is_symlink():
|
||||
symlink_path.unlink()
|
||||
symlink_path.symlink_to(target_name)
|
||||
print(f"Symlink created: {symlink_path} -> {target_name}")
|
||||
except OSError as e:
|
||||
print(f"(skipping symlink: {e})")
|
||||
|
||||
|
||||
def download_with_progress(url: str, dest_path: Path):
|
||||
"""Download file with progress indicator"""
|
||||
print(f"Downloading to: {dest_path}")
|
||||
|
||||
def progress_hook(count, block_size, total_size):
|
||||
percent = int(count * block_size * 100 / total_size)
|
||||
mb_done = count * block_size / (1024 * 1024)
|
||||
mb_total = total_size / (1024 * 1024)
|
||||
sys.stdout.write(f"\r Progress: {percent}% ({mb_done:.1f}/{mb_total:.1f} MB)")
|
||||
sys.stdout.flush()
|
||||
|
||||
urllib.request.urlretrieve(url, dest_path, progress_hook)
|
||||
print("\n Download complete!")
|
||||
|
||||
def main():
|
||||
# Determine model type
|
||||
model_type = sys.argv[1] if len(sys.argv) > 1 else 'vit_b'
|
||||
|
||||
if model_type not in SAM_MODELS:
|
||||
print(f"Unknown model type: {model_type}")
|
||||
print(f"Available: {', '.join(SAM_MODELS.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
model_info = SAM_MODELS[model_type]
|
||||
|
||||
# Determine models directory
|
||||
# Check if running in Docker (mounted volume) or locally
|
||||
models_dir = Path('/app/data/models')
|
||||
if not models_dir.exists():
|
||||
models_dir = Path(__file__).parent.parent / 'data' / 'models'
|
||||
|
||||
models_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
dest_path = models_dir / model_info['filename']
|
||||
|
||||
print("=" * 60)
|
||||
print("SAM Model Downloader")
|
||||
print("=" * 60)
|
||||
print(f"Model: SAM {model_type.upper()}")
|
||||
print(f"Size: {model_info['size']}")
|
||||
print(f"License: Apache 2.0 (commercial use OK)")
|
||||
print("=" * 60)
|
||||
|
||||
# Check if already downloaded
|
||||
if dest_path.exists():
|
||||
print(f"\nModel already exists at: {dest_path}")
|
||||
print("To re-download, delete the file first.")
|
||||
|
||||
create_symlink(models_dir / 'sam_model.pth', dest_path.name)
|
||||
return
|
||||
|
||||
print(f"\nDownloading SAM {model_type.upper()} ({model_info['size']})...")
|
||||
print("This is a one-time download. The model will persist across rebuilds.")
|
||||
print()
|
||||
|
||||
try:
|
||||
download_with_progress(model_info['url'], dest_path)
|
||||
|
||||
symlink_path = models_dir / 'sam_model.pth'
|
||||
create_symlink(symlink_path, dest_path.name)
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("SUCCESS!")
|
||||
print(f"Model saved to: {dest_path}")
|
||||
print("=" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nError downloading model: {e}")
|
||||
if dest_path.exists():
|
||||
dest_path.unlink()
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+210
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download sample classical eye images and import them into the patch catalog.
|
||||
|
||||
These are public domain images from Wikimedia Commons of classical sculptures.
|
||||
Run this script to populate the eye catalog with example eyes.
|
||||
|
||||
Usage:
|
||||
cd /home/user/EditmaskwithAI
|
||||
python scripts/download_sample_eyes.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
# Add backend to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'backend'))
|
||||
|
||||
# Sample eye images - public domain classical sculpture references
|
||||
# These URLs point to Wikimedia Commons images of ancient sculptures
|
||||
SAMPLE_EYES = [
|
||||
{
|
||||
'name': 'Greek Serene - Left',
|
||||
'url': 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Head_Hygieia_BM_550.jpg/220px-Head_Hygieia_BM_550.jpg',
|
||||
'tags': 'greek,serene,left,marble',
|
||||
'category': 'eyes',
|
||||
'description': 'Classical Greek style eye from Hygieia statue'
|
||||
},
|
||||
{
|
||||
'name': 'Roman Portrait - Right',
|
||||
'url': 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Bust_of_Emperor_Philip_the_Arab_-_Hermitage_Museum.jpg/220px-Bust_of_Emperor_Philip_the_Arab_-_Hermitage_Museum.jpg',
|
||||
'tags': 'roman,portrait,right,marble',
|
||||
'category': 'eyes',
|
||||
'description': 'Roman portrait style eye'
|
||||
},
|
||||
{
|
||||
'name': 'Greek Classical - Pair',
|
||||
'url': 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Marble_head_of_a_veiled_woman_MET_DT229963.jpg/220px-Marble_head_of_a_veiled_woman_MET_DT229963.jpg',
|
||||
'tags': 'greek,classical,pair,marble,veiled',
|
||||
'category': 'eyes',
|
||||
'description': 'Greek classical style veiled woman'
|
||||
},
|
||||
]
|
||||
|
||||
def download_image(url: str) -> bytes:
|
||||
"""Download image from URL and return bytes"""
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; EyeCatalogDownloader/1.0)'
|
||||
}
|
||||
response = requests.get(url, headers=headers, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.content
|
||||
|
||||
def create_patch_directory(patch_id: int, data_dir: Path) -> Path:
|
||||
"""Create directory for patch files"""
|
||||
patch_dir = data_dir / 'patches' / str(patch_id)
|
||||
patch_dir.mkdir(parents=True, exist_ok=True)
|
||||
return patch_dir
|
||||
|
||||
def save_patch_and_thumbnail(image_bytes: bytes, patch_dir: Path) -> tuple:
|
||||
"""Save patch image and create thumbnail"""
|
||||
# Open image
|
||||
img = Image.open(BytesIO(image_bytes)).convert('RGBA')
|
||||
|
||||
# Save full size
|
||||
patch_path = patch_dir / 'patch.png'
|
||||
img.save(patch_path, 'PNG')
|
||||
|
||||
# Create thumbnail (max 200x200)
|
||||
thumb = img.copy()
|
||||
thumb.thumbnail((200, 200), Image.Resampling.LANCZOS)
|
||||
thumb_path = patch_dir / 'thumbnail.png'
|
||||
thumb.save(thumb_path, 'PNG')
|
||||
|
||||
return patch_path, thumb_path, img.size
|
||||
|
||||
def import_eye_to_database(db_path: Path, eye_data: dict, patch_path: str, thumb_path: str, width: int, height: int) -> int:
|
||||
"""Insert patch record into database"""
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO patches (name, description, source_type, category, tags, file_path, thumbnail_path, width, height)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
eye_data['name'],
|
||||
eye_data.get('description', ''),
|
||||
'imported',
|
||||
eye_data['category'],
|
||||
eye_data['tags'],
|
||||
str(patch_path),
|
||||
str(thumb_path),
|
||||
width,
|
||||
height
|
||||
))
|
||||
|
||||
patch_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return patch_id
|
||||
|
||||
def main():
|
||||
# Determine paths - handle both Docker and local environments
|
||||
# In Docker: script is at /scripts/, data is at /app/data/
|
||||
# Locally: script is at ./scripts/, data is at ./data/
|
||||
docker_data_dir = Path('/app/data')
|
||||
local_data_dir = Path(__file__).parent.parent / 'data'
|
||||
|
||||
if docker_data_dir.exists():
|
||||
data_dir = docker_data_dir
|
||||
else:
|
||||
data_dir = local_data_dir
|
||||
|
||||
db_path = data_dir / 'ai_photo_edit.db'
|
||||
|
||||
# Check if database exists
|
||||
if not db_path.exists():
|
||||
print(f"Database not found at {db_path}")
|
||||
print("Attempting to initialize database...")
|
||||
# Try to import and initialize database
|
||||
try:
|
||||
sys.path.insert(0, str(Path('/app')))
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'backend'))
|
||||
from app.database import init_db
|
||||
init_db()
|
||||
print("Database initialized successfully.")
|
||||
except Exception as e:
|
||||
print(f"Could not initialize database: {e}")
|
||||
print("Please start the backend first to initialize the database.")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Using database: {db_path}")
|
||||
print(f"Data directory: {data_dir}")
|
||||
print()
|
||||
|
||||
# Check if patches table exists
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='patches'")
|
||||
if not cursor.fetchone():
|
||||
print("Patches table not found. Creating it...")
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS patches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR NOT NULL,
|
||||
description TEXT,
|
||||
source_type VARCHAR NOT NULL,
|
||||
category VARCHAR,
|
||||
tags TEXT,
|
||||
file_path VARCHAR,
|
||||
thumbnail_path VARCHAR,
|
||||
width INTEGER,
|
||||
height INTEGER,
|
||||
source_project_id INTEGER,
|
||||
source_edit_id INTEGER,
|
||||
user_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
successful = 0
|
||||
failed = 0
|
||||
|
||||
for eye_data in SAMPLE_EYES:
|
||||
print(f"Downloading: {eye_data['name']}...")
|
||||
|
||||
try:
|
||||
# Download image
|
||||
image_bytes = download_image(eye_data['url'])
|
||||
|
||||
# Get next patch ID
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT COALESCE(MAX(id), 0) + 1 FROM patches")
|
||||
next_id = cursor.fetchone()[0]
|
||||
conn.close()
|
||||
|
||||
# Create directory and save files
|
||||
patch_dir = create_patch_directory(next_id, data_dir)
|
||||
patch_path, thumb_path, (width, height) = save_patch_and_thumbnail(image_bytes, patch_dir)
|
||||
|
||||
# Import to database
|
||||
patch_id = import_eye_to_database(db_path, eye_data, patch_path, thumb_path, width, height)
|
||||
|
||||
print(f" ✓ Imported as patch #{patch_id} ({width}x{height})")
|
||||
successful += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
failed += 1
|
||||
|
||||
print()
|
||||
print(f"Done! Imported {successful} eyes, {failed} failed.")
|
||||
print()
|
||||
print("You can now see the eyes in the Eye Catalog panel in the web UI.")
|
||||
print("To add your own eyes:")
|
||||
print(" 1. Click '+ Add Eye' in the Eye Catalog")
|
||||
print(" 2. Upload a PNG image (transparency works best)")
|
||||
print(" 3. Give it a name and tags")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download U2Net model for background removal.
|
||||
|
||||
U2Net is a deep learning model for salient object detection,
|
||||
commonly used for background removal tasks.
|
||||
|
||||
Usage:
|
||||
python download_u2net_model.py [model_type]
|
||||
|
||||
Model types:
|
||||
u2net - Full U2Net model (~176MB, best quality)
|
||||
u2netp - Lightweight U2Net (~4MB, faster, good quality)
|
||||
u2net_human_seg - Optimized for human segmentation (~176MB)
|
||||
|
||||
Default: u2netp (good balance of quality and speed)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
# Model URLs (from official U2Net repository releases)
|
||||
MODEL_URLS = {
|
||||
'u2net': {
|
||||
'url': 'https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2net.onnx',
|
||||
'filename': 'u2net.onnx',
|
||||
'size_mb': 176
|
||||
},
|
||||
'u2netp': {
|
||||
'url': 'https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2netp.onnx',
|
||||
'filename': 'u2netp.onnx',
|
||||
'size_mb': 4
|
||||
},
|
||||
'u2net_human_seg': {
|
||||
'url': 'https://github.com/danielgatis/rembg/releases/download/v0.0.0/u2net_human_seg.onnx',
|
||||
'filename': 'u2net_human_seg.onnx',
|
||||
'size_mb': 176
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def download_with_progress(url: str, dest_path: Path, expected_size_mb: int):
|
||||
"""Download file with progress indicator."""
|
||||
|
||||
print(f"Downloading from: {url}")
|
||||
print(f"Expected size: ~{expected_size_mb}MB")
|
||||
|
||||
def progress_hook(count, block_size, total_size):
|
||||
if total_size > 0:
|
||||
percent = min(100, count * block_size * 100 // total_size)
|
||||
downloaded_mb = count * block_size / (1024 * 1024)
|
||||
total_mb = total_size / (1024 * 1024)
|
||||
sys.stdout.write(f"\rProgress: {percent}% ({downloaded_mb:.1f}/{total_mb:.1f} MB)")
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
urllib.request.urlretrieve(url, str(dest_path), progress_hook)
|
||||
print("\nDownload complete!")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"\nDownload failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
# Determine model type
|
||||
model_type = 'u2netp' # Default to lightweight model
|
||||
if len(sys.argv) > 1:
|
||||
model_type = sys.argv[1].lower()
|
||||
|
||||
if model_type not in MODEL_URLS:
|
||||
print(f"Unknown model type: {model_type}")
|
||||
print(f"Available models: {', '.join(MODEL_URLS.keys())}")
|
||||
sys.exit(1)
|
||||
|
||||
model_info = MODEL_URLS[model_type]
|
||||
|
||||
# Determine models directory
|
||||
# Check if running in Docker container
|
||||
if os.path.exists('/app/data/models'):
|
||||
models_dir = Path('/app/data/models')
|
||||
else:
|
||||
# Local development
|
||||
script_dir = Path(__file__).parent
|
||||
models_dir = script_dir.parent / 'data' / 'models'
|
||||
|
||||
models_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
dest_path = models_dir / model_info['filename']
|
||||
|
||||
# Check if already downloaded
|
||||
if dest_path.exists():
|
||||
print(f"Model already exists at: {dest_path}")
|
||||
print("Delete the file to re-download.")
|
||||
return
|
||||
|
||||
print(f"Downloading U2Net model: {model_type}")
|
||||
print(f"Destination: {dest_path}")
|
||||
print("")
|
||||
|
||||
success = download_with_progress(
|
||||
model_info['url'],
|
||||
dest_path,
|
||||
model_info['size_mb']
|
||||
)
|
||||
|
||||
if success:
|
||||
# Create symlink for easier access
|
||||
symlink_path = models_dir / 'u2net.onnx'
|
||||
if not symlink_path.exists() or symlink_path.is_symlink():
|
||||
if symlink_path.is_symlink():
|
||||
symlink_path.unlink()
|
||||
try:
|
||||
symlink_path.symlink_to(dest_path.name)
|
||||
print(f"Created symlink: {symlink_path} -> {dest_path.name}")
|
||||
except OSError:
|
||||
# Symlinks may not work on all systems
|
||||
pass
|
||||
|
||||
print(f"\nU2Net model ({model_type}) downloaded successfully!")
|
||||
print(f"Location: {dest_path}")
|
||||
print("\nYou can now use background removal in the application.")
|
||||
else:
|
||||
print("\nFailed to download model. Please try again or download manually from:")
|
||||
print(f" {model_info['url']}")
|
||||
print(f" Save to: {dest_path}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GPU setup script — runs at container startup.
|
||||
Uses the same detection logic as the backend (gpu_detect.py) to show
|
||||
exactly which models will be used before the server starts.
|
||||
Non-fatal: any failure just prints a warning and startup continues.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
print("Detecting GPU capabilities…")
|
||||
|
||||
try:
|
||||
import torch
|
||||
except ImportError:
|
||||
print("⚠ PyTorch not installed — GPU detection skipped")
|
||||
return
|
||||
|
||||
if torch.cuda.is_available():
|
||||
props = torch.cuda.get_device_properties(0)
|
||||
free_b, total_b = torch.cuda.mem_get_info(0)
|
||||
vram_total = total_b / (1024 ** 3)
|
||||
vram_free = free_b / (1024 ** 3)
|
||||
major, minor = props.major, props.minor
|
||||
cc = f"{major}.{minor}"
|
||||
|
||||
fp16 = major >= 6
|
||||
bf16 = major >= 8
|
||||
fp8 = major > 8 or (major == 8 and minor >= 9)
|
||||
int8 = major >= 7
|
||||
tc = major >= 7
|
||||
|
||||
flags = []
|
||||
if fp16: flags.append("fp16")
|
||||
if bf16: flags.append("bf16")
|
||||
if fp8: flags.append("fp8")
|
||||
if int8: flags.append("int8")
|
||||
if tc: flags.append("tensor-cores")
|
||||
|
||||
print(f"✓ GPU : {props.name}")
|
||||
print(f" VRAM : {vram_total:.1f} GB total | {vram_free:.1f} GB free")
|
||||
print(f" Compute : CC {cc} ({', '.join(flags) or 'fp32 only'})")
|
||||
|
||||
if major < 6:
|
||||
print(f" ⚠ Pre-Pascal (CC {cc}): using fp32 — effective VRAM budget halved")
|
||||
|
||||
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
|
||||
print("✓ Apple Silicon MPS GPU detected (fp32 mode)")
|
||||
vram_total = vram_free = 0.0
|
||||
else:
|
||||
print("⚠ No GPU detected — AI inference will use CPU (very slow)")
|
||||
vram_total = vram_free = 0.0
|
||||
|
||||
provider = os.environ.get("AI_PROVIDER", "").lower()
|
||||
if provider != "local_gpu":
|
||||
print(f" AI_PROVIDER={provider!r} — local GPU not active, skipping model selection")
|
||||
return
|
||||
|
||||
# Import and run the full detection to show what was selected
|
||||
try:
|
||||
sys.path.insert(0, "/app")
|
||||
from app.services.gpu_detect import detect_gpu
|
||||
info = detect_gpu()
|
||||
|
||||
print(f"\n Effective VRAM : {info.effective_vram_gb:.1f} GB (tier: {info.tier})")
|
||||
print("\n Model selection:")
|
||||
printed: set = set()
|
||||
for op, spec in info.recommended.items():
|
||||
if spec is None:
|
||||
print(f" {op:<12} → (none — will use existing upscaler)")
|
||||
elif spec.model_id not in printed:
|
||||
print(f" {op:<12} → [{spec.family}] {spec.model_id}")
|
||||
print(f" mem_opt={spec.memory_opt} res={spec.native_res}px ~{spec.vram_fp16_gb}GB fp16")
|
||||
printed.add(spec.model_id)
|
||||
else:
|
||||
print(f" {op:<12} → (same as above: {spec.model_id})")
|
||||
|
||||
for w in info.warnings:
|
||||
print(f"\n ⚠ {w}")
|
||||
|
||||
auto_dl = os.environ.get("AUTO_DOWNLOAD_MODELS", "true").lower()
|
||||
print()
|
||||
if auto_dl == "true":
|
||||
print(" AUTO_DOWNLOAD_MODELS=true")
|
||||
print(" → Model files will download in background at startup.")
|
||||
print(" → First request loads from local disk (20-60s, not internet).")
|
||||
print(" → Track progress: GET /api/gpu/prefetch-status")
|
||||
else:
|
||||
print(" AUTO_DOWNLOAD_MODELS=false — models download on first request.")
|
||||
|
||||
except Exception as exc:
|
||||
print(f" (Could not run full detection: {exc})")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
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,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Initialize the database before other startup scripts run.
|
||||
|
||||
This ensures the database exists and has all required tables
|
||||
before download_sample_eyes.py tries to use it.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add backend to path - handle both Docker and local environments
|
||||
# In Docker: backend is at /app/
|
||||
# Locally: backend is at ./backend/
|
||||
if Path('/app').exists():
|
||||
sys.path.insert(0, '/app')
|
||||
else:
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'backend'))
|
||||
|
||||
def main():
|
||||
# Import after path setup
|
||||
from app.database import engine, Base, init_db
|
||||
from app.models import project, user, patch
|
||||
|
||||
print("Initializing database...")
|
||||
|
||||
# Create all tables
|
||||
init_db()
|
||||
|
||||
# Verify database was created - check both Docker and local paths
|
||||
docker_db_path = Path('/app/data/ai_photo_edit.db')
|
||||
local_db_path = Path('./data/ai_photo_edit.db')
|
||||
|
||||
if docker_db_path.exists():
|
||||
print(f"✓ Database initialized at: {docker_db_path}")
|
||||
elif local_db_path.exists():
|
||||
print(f"✓ Database initialized at: {local_db_path}")
|
||||
else:
|
||||
print("⚠ Database file not found at expected locations, but tables may still be created")
|
||||
|
||||
print("Database initialization complete.")
|
||||
|
||||
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