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:
Claude
2026-06-26 05:48:43 +00:00
parent b4e8ba2a79
commit 084922afaa
431 changed files with 87396 additions and 77 deletions
+108
View File
@@ -0,0 +1,108 @@
# Classic Eyes Scripts
This directory contains scripts for generating and importing classic eye images into the AI Photo Edit patch library.
## Overview
The scripts generate a variety of classic eye styles that can be used as reusable patches for photo editing:
- **Realistic eyes** - Detailed eyes with gradient irises and realistic highlights
- **Anime eyes** - Large, expressive eyes in anime style with prominent highlights
- **Cartoon eyes** - Simple, bold cartoon-style eyes
Each style is available in multiple iris colors: blue, green, brown, hazel, grey, and amber.
## Scripts
### 1. `generate_classic_eyes_ppm.py`
Generates classic eye images using only the Python standard library (no dependencies required).
```bash
python scripts/generate_classic_eyes_ppm.py
```
This creates PPM format images in `data/classic_eyes_ppm/`. PPM is a simple image format that can be converted to PNG later.
### 2. `download_classic_eyes.py`
Full-featured script that generates eyes and imports them directly into the patch library. Requires PIL/Pillow.
```bash
# Run inside the backend container
docker exec -it ai-photo-edit-backend python /app/../scripts/download_classic_eyes.py
# Or with the API running
python scripts/download_classic_eyes.py --api --base-url http://localhost:8101
# Or save to a directory for later import
python scripts/download_classic_eyes.py --output-dir ./my_eyes
```
### 3. `startup_import_eyes.py`
Converts PPM files to PNG and imports them into the patch library. Run this inside the backend container.
```bash
docker exec -it ai-photo-edit-backend python /app/../scripts/startup_import_eyes.py
```
### 4. `import_saved_eyes.py`
Import previously saved eye images from a directory.
```bash
python scripts/import_saved_eyes.py /path/to/saved/eyes
```
## Quick Start
1. **Generate the eye images** (no dependencies needed):
```bash
python scripts/generate_classic_eyes_ppm.py
```
2. **Start the application**:
```bash
docker compose up -d
```
3. **Import the eyes**:
```bash
docker exec -it ai-photo-edit-backend python /scripts/startup_import_eyes.py
```
4. **Verify in the app**: Open the patch library in the UI to see the imported classic eyes.
## Generated Eyes
| Style | Colors Available | Size |
|-----------|-------------------------------------------|--------|
| Realistic | Blue, Green, Brown, Hazel, Grey, Amber | 200x200|
| Anime | Blue, Green, Brown, Hazel, Grey, Amber | 200x200|
| Cartoon | Blue, Green, Brown, Hazel, Grey, Amber | 200x200|
Total: 18 unique eye variants
## Extending
To add more eye styles or colors, edit the `generate_classic_eyes_ppm.py` or `download_classic_eyes.py` scripts:
```python
# Add new color
colors["purple"] = (128, 0, 128)
# Add new style in create_classic_eye() function
elif style == "fantasy":
# Your custom eye drawing code
pass
```
## File Formats
- **PPM**: Portable Pixmap format - simple, universal, generated without dependencies
- **PNG**: Preferred format for the patch library - converted from PPM using PIL
## License
The generated eye images are created programmatically and are free to use without restrictions.
+629
View File
@@ -0,0 +1,629 @@
#!/usr/bin/env python3
"""
Download and import classic eye images into the patch library.
This script downloads open-source/public domain eye images and imports them
into the AI Photo Edit patch library for use as reusable eye patches.
Sources:
- OpenGameArt.org (CC0/Public Domain game assets)
- Generated stylized eyes using PIL
- Public domain vintage illustrations
Usage:
# Run inside the backend container or with backend dependencies:
python scripts/download_classic_eyes.py
# Or run via API when app is running:
python scripts/download_classic_eyes.py --api --base-url http://localhost:8101
"""
import os
import sys
import io
import json
import argparse
from pathlib import Path
from datetime import datetime
# Try to import dependencies
try:
from PIL import Image, ImageDraw, ImageFilter
HAS_PIL = True
except ImportError:
HAS_PIL = False
print("Warning: PIL not available. Install with: pip install Pillow")
try:
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "backend"))
try:
from sqlalchemy.orm import Session
from app.database import SessionLocal, engine, Base
from app.models.patch import Patch
from app.services.patch_library import PatchLibraryService
HAS_BACKEND = True
except ImportError:
HAS_BACKEND = False
print("Warning: Backend modules not available. Use --api mode or run inside backend container.")
# Public domain eye image sources (CC0/Public Domain)
CLASSIC_EYE_URLS = [
# OpenGameArt style eyes - these are placeholder URLs
# In production, you would use actual public domain image URLs
]
# We'll generate classic stylized eyes instead since downloading from external
# sources can be unreliable. These are better quality and guaranteed available.
def create_classic_eye(
size: tuple = (200, 200),
iris_color: tuple = (70, 130, 180), # Steel blue
pupil_size_ratio: float = 0.3,
iris_size_ratio: float = 0.7,
style: str = "realistic"
) -> Image.Image:
"""
Generate a classic stylized eye image.
Args:
size: Output image size (width, height)
iris_color: RGB color for the iris
pupil_size_ratio: Ratio of pupil to iris
iris_size_ratio: Ratio of iris to eye
style: "realistic", "anime", "cartoon", "vintage"
Returns:
PIL Image with transparent background
"""
img = Image.new('RGBA', size, (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
center_x, center_y = size[0] // 2, size[1] // 2
eye_radius = min(size) // 2 - 5
iris_radius = int(eye_radius * iris_size_ratio)
pupil_radius = int(iris_radius * pupil_size_ratio)
if style == "realistic":
# White of the eye (sclera) with slight pink tint
sclera_color = (250, 245, 240, 255)
draw.ellipse(
[center_x - eye_radius, center_y - eye_radius,
center_x + eye_radius, center_y + eye_radius],
fill=sclera_color,
outline=(180, 160, 150, 255),
width=2
)
# Iris with gradient effect
for i in range(iris_radius, 0, -2):
ratio = i / iris_radius
color = (
int(iris_color[0] * ratio + 40 * (1 - ratio)),
int(iris_color[1] * ratio + 40 * (1 - ratio)),
int(iris_color[2] * ratio + 40 * (1 - ratio)),
255
)
draw.ellipse(
[center_x - i, center_y - i, center_x + i, center_y + i],
fill=color
)
# Pupil
draw.ellipse(
[center_x - pupil_radius, center_y - pupil_radius,
center_x + pupil_radius, center_y + pupil_radius],
fill=(10, 10, 10, 255)
)
# Highlight/reflection
highlight_x = center_x - pupil_radius // 2
highlight_y = center_y - pupil_radius // 2
highlight_radius = pupil_radius // 3
draw.ellipse(
[highlight_x - highlight_radius, highlight_y - highlight_radius,
highlight_x + highlight_radius, highlight_y + highlight_radius],
fill=(255, 255, 255, 200)
)
elif style == "anime":
# Large iris, small pupil, big highlight - anime style
iris_radius = int(eye_radius * 0.85)
# Sclera
draw.ellipse(
[center_x - eye_radius, center_y - eye_radius,
center_x + eye_radius, center_y + eye_radius],
fill=(255, 255, 255, 255),
outline=(0, 0, 0, 255),
width=3
)
# Large iris
draw.ellipse(
[center_x - iris_radius, center_y - iris_radius,
center_x + iris_radius, center_y + iris_radius],
fill=iris_color + (255,)
)
# Pupil
pupil_radius = int(iris_radius * 0.25)
draw.ellipse(
[center_x - pupil_radius, center_y - pupil_radius,
center_x + pupil_radius, center_y + pupil_radius],
fill=(0, 0, 0, 255)
)
# Large anime-style highlight
hl_x, hl_y = center_x - iris_radius // 3, center_y - iris_radius // 3
hl_r = iris_radius // 3
draw.ellipse(
[hl_x - hl_r, hl_y - hl_r, hl_x + hl_r, hl_y + hl_r],
fill=(255, 255, 255, 255)
)
# Secondary smaller highlight
hl2_x, hl2_y = center_x + iris_radius // 4, center_y + iris_radius // 4
hl2_r = iris_radius // 6
draw.ellipse(
[hl2_x - hl2_r, hl2_y - hl2_r, hl2_x + hl2_r, hl2_y + hl2_r],
fill=(255, 255, 255, 200)
)
elif style == "cartoon":
# Simple cartoon eye
# Sclera
draw.ellipse(
[center_x - eye_radius, center_y - eye_radius,
center_x + eye_radius, center_y + eye_radius],
fill=(255, 255, 255, 255),
outline=(0, 0, 0, 255),
width=4
)
# Simple colored iris
draw.ellipse(
[center_x - iris_radius, center_y - iris_radius,
center_x + iris_radius, center_y + iris_radius],
fill=iris_color + (255,),
outline=(0, 0, 0, 255),
width=2
)
# Pupil
draw.ellipse(
[center_x - pupil_radius, center_y - pupil_radius,
center_x + pupil_radius, center_y + pupil_radius],
fill=(0, 0, 0, 255)
)
# Highlight
hl_r = pupil_radius // 2
draw.ellipse(
[center_x - pupil_radius - hl_r, center_y - pupil_radius - hl_r,
center_x - pupil_radius + hl_r, center_y - pupil_radius + hl_r],
fill=(255, 255, 255, 255)
)
elif style == "vintage":
# Vintage engraving style eye
# Multiple concentric circles for hatching effect
draw.ellipse(
[center_x - eye_radius, center_y - eye_radius,
center_x + eye_radius, center_y + eye_radius],
fill=(245, 235, 220, 255),
outline=(80, 60, 40, 255),
width=2
)
# Iris with hatching-like rings
for i in range(iris_radius, pupil_radius, -4):
ratio = (i - pupil_radius) / (iris_radius - pupil_radius)
alpha = int(150 + 100 * ratio)
draw.ellipse(
[center_x - i, center_y - i, center_x + i, center_y + i],
outline=(60 + int(iris_color[0] * 0.3),
50 + int(iris_color[1] * 0.3),
40 + int(iris_color[2] * 0.3), alpha),
width=1
)
# Dark pupil
draw.ellipse(
[center_x - pupil_radius, center_y - pupil_radius,
center_x + pupil_radius, center_y + pupil_radius],
fill=(20, 15, 10, 255)
)
# Apply slight blur for more natural look
if style in ["realistic", "vintage"]:
img = img.filter(ImageFilter.GaussianBlur(radius=0.5))
return img
def generate_eye_variants() -> list:
"""
Generate a set of classic eye variants with different colors and styles.
Returns:
List of (name, description, tags, image) tuples
"""
variants = []
# Eye colors
colors = {
"blue": (70, 130, 180),
"green": (60, 140, 90),
"brown": (139, 90, 43),
"hazel": (150, 120, 70),
"grey": (120, 130, 140),
"amber": (180, 130, 50),
"violet": (138, 43, 226),
"black": (30, 30, 35),
}
# Styles
styles = ["realistic", "anime", "cartoon", "vintage"]
# Sizes
sizes = {
"small": (100, 100),
"medium": (200, 200),
"large": (300, 300),
}
# Generate all combinations for medium size, main styles
for style in styles:
for color_name, color_rgb in colors.items():
size = sizes["medium"]
img = create_classic_eye(
size=size,
iris_color=color_rgb,
style=style
)
name = f"Classic {style.title()} Eye - {color_name.title()}"
description = f"A {style} style eye with {color_name} iris color"
tags = f"eye,classic,{style},{color_name},medium"
variants.append((name, description, tags, img))
# Add some extra size variants for most popular combinations
popular = [
("blue", "realistic"),
("brown", "realistic"),
("green", "realistic"),
("blue", "anime"),
("green", "anime"),
]
for color_name, style in popular:
color_rgb = colors[color_name]
for size_name, size in sizes.items():
if size_name == "medium":
continue # Already generated
img = create_classic_eye(
size=size,
iris_color=color_rgb,
style=style
)
name = f"Classic {style.title()} Eye - {color_name.title()} ({size_name})"
description = f"A {size_name} {style} style eye with {color_name} iris"
tags = f"eye,classic,{style},{color_name},{size_name}"
variants.append((name, description, tags, img))
return variants
def download_external_eyes() -> list:
"""
Download eye images from external public domain sources.
Returns:
List of (name, description, tags, image) tuples
"""
if not HAS_REQUESTS or not HAS_PIL:
print(" Skipping external downloads (missing dependencies)")
return []
results = []
# OpenGameArt and other CC0 sources
# These are example URLs - in production, curate actual public domain images
external_sources = [
{
"url": "https://opengameart.org/sites/default/files/eye_0.png",
"name": "OpenGameArt Eye Sprite",
"description": "Pixel art style eye from OpenGameArt (CC0)",
"tags": "eye,pixel,game,sprite,public_domain"
},
]
for source in external_sources:
try:
response = requests.get(source["url"], timeout=10)
if response.status_code == 200:
img = Image.open(io.BytesIO(response.content)).convert('RGBA')
results.append((
source["name"],
source["description"],
source["tags"],
img
))
print(f" Downloaded: {source['name']}")
else:
print(f" Failed to download {source['name']}: HTTP {response.status_code}")
except Exception as e:
print(f" Error downloading {source['name']}: {e}")
return results
def import_eyes_to_library(eyes: list, db: Session, patch_service: PatchLibraryService):
"""
Import eye images into the patch library database.
Args:
eyes: List of (name, description, tags, image) tuples
db: Database session
patch_service: PatchLibraryService instance
"""
imported_count = 0
for name, description, tags, img in eyes:
try:
# Check if patch with same name already exists
existing = db.query(Patch).filter(Patch.name == name).first()
if existing:
print(f" Skipping (exists): {name}")
continue
# Create database record first to get ID
patch = Patch(
name=name,
description=description,
source_type="imported",
width=img.width,
height=img.height,
tags=tags,
category="eye",
is_public=True,
file_path="", # Will update after saving
thumbnail_path=""
)
db.add(patch)
db.flush() # Get the ID
# Save image file
patch_path = patch_service.get_patch_path(patch.id)
img.save(patch_path, 'PNG')
# Create thumbnail
thumb_path = patch_service.get_thumbnail_path(patch.id)
patch_service.create_thumbnail(patch_path, thumb_path)
# Update paths in database
patch.file_path = f"patch_library/{patch.id}.png"
patch.thumbnail_path = f"patch_library/{patch.id}_thumb.png"
db.commit()
imported_count += 1
print(f" Imported: {name} (ID: {patch.id})")
except Exception as e:
db.rollback()
print(f" Error importing {name}: {e}")
return imported_count
def import_via_api(eyes: list, base_url: str) -> int:
"""
Import eyes via the REST API.
Args:
eyes: List of (name, description, tags, image) tuples
base_url: Base URL of the API (e.g., http://localhost:8101)
Returns:
Number of successfully imported eyes
"""
if not HAS_REQUESTS:
print("Error: requests library required for API mode")
return 0
imported = 0
for name, description, tags, img in eyes:
try:
# Convert image to bytes
img_buffer = io.BytesIO()
img.save(img_buffer, format='PNG')
img_buffer.seek(0)
# Upload via API
files = {'file': (f'{name}.png', img_buffer, 'image/png')}
data = {
'name': name,
'description': description,
'tags': tags,
'category': 'eye',
'source_type': 'imported'
}
response = requests.post(
f"{base_url}/patches/",
files=files,
data=data,
timeout=30
)
if response.status_code in [200, 201]:
patch_id = response.json().get('id', 'unknown')
print(f" Imported: {name} (ID: {patch_id})")
imported += 1
elif response.status_code == 409:
print(f" Skipping (exists): {name}")
else:
print(f" Failed to import {name}: HTTP {response.status_code}")
except Exception as e:
print(f" Error importing {name}: {e}")
return imported
def save_eyes_to_files(eyes: list, output_dir: Path) -> int:
"""
Save generated eyes as PNG files for manual import later.
Args:
eyes: List of (name, description, tags, image) tuples
output_dir: Directory to save images
Returns:
Number of saved files
"""
output_dir.mkdir(parents=True, exist_ok=True)
saved = 0
# Create a metadata file
metadata = []
for name, description, tags, img in eyes:
try:
# Create safe filename
safe_name = name.replace(' ', '_').replace('-', '_').lower()
safe_name = ''.join(c for c in safe_name if c.isalnum() or c == '_')
filename = f"{safe_name}.png"
filepath = output_dir / filename
img.save(filepath, 'PNG')
metadata.append({
'filename': filename,
'name': name,
'description': description,
'tags': tags,
'width': img.width,
'height': img.height
})
saved += 1
except Exception as e:
print(f" Error saving {name}: {e}")
# Write metadata JSON
metadata_path = output_dir / "metadata.json"
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
print(f" Saved metadata to: {metadata_path}")
return saved
def main():
"""Main function to download and import classic eyes."""
parser = argparse.ArgumentParser(description='Download and import classic eye images')
parser.add_argument('--api', action='store_true', help='Use API mode to import')
parser.add_argument('--base-url', default='http://localhost:8101', help='API base URL')
parser.add_argument('--output-dir', help='Save images to directory instead of importing')
parser.add_argument('--skip-external', action='store_true', help='Skip downloading external images')
args = parser.parse_args()
print("=" * 60)
print("Classic Eye Importer for AI Photo Edit")
print("=" * 60)
if not HAS_PIL:
print("\nError: PIL/Pillow is required. Install with: pip install Pillow")
print("Or run this script inside the backend container.")
sys.exit(1)
all_eyes = []
# Generate stylized eyes
print("\n[1/3] Generating classic stylized eyes...")
generated_eyes = generate_eye_variants()
print(f" Generated {len(generated_eyes)} eye variants")
all_eyes.extend(generated_eyes)
# Download external public domain eyes
if not args.skip_external:
print("\n[2/3] Downloading external public domain eyes...")
external_eyes = download_external_eyes()
print(f" Downloaded {len(external_eyes)} external eyes")
all_eyes.extend(external_eyes)
else:
print("\n[2/3] Skipping external downloads (--skip-external)")
# Determine import method
print(f"\n[3/3] Processing {len(all_eyes)} eyes...")
if args.output_dir:
# Save to files
output_dir = Path(args.output_dir)
print(f" Saving to directory: {output_dir}")
saved = save_eyes_to_files(all_eyes, output_dir)
print(f" Saved {saved} eye images to {output_dir}")
elif args.api:
# Import via API
print(f" Using API mode: {args.base_url}")
imported = import_via_api(all_eyes, args.base_url)
print(f"\n Successfully imported: {imported}")
print(f" Skipped/Failed: {len(all_eyes) - imported}")
elif HAS_BACKEND:
# Direct database import
print(" Using direct database import...")
Base.metadata.create_all(bind=engine)
db = SessionLocal()
data_dir = os.environ.get("DATA_DIR", str(Path(__file__).parent.parent / "data"))
patch_service = PatchLibraryService(data_dir)
print(f" Patch library dir: {patch_service.patch_library_dir}")
imported = import_eyes_to_library(all_eyes, db, patch_service)
# Summary
print("\n" + "=" * 60)
print("Import Complete!")
print(f" Total eyes processed: {len(all_eyes)}")
print(f" Successfully imported: {imported}")
print(f" Skipped (duplicates): {len(all_eyes) - imported}")
print("=" * 60)
# Show sample of imported eyes
print("\nSample of imported eyes:")
samples = db.query(Patch).filter(Patch.category == "eye").limit(5).all()
for p in samples:
print(f" - {p.name} ({p.width}x{p.height}) [ID: {p.id}]")
db.close()
else:
# Fallback: save to files
output_dir = Path(__file__).parent.parent / "data" / "classic_eyes_import"
print(f" Backend not available. Saving to: {output_dir}")
saved = save_eyes_to_files(all_eyes, output_dir)
print(f"\n Saved {saved} eye images")
print(f"\n To import later, run inside backend container:")
print(f" python scripts/import_saved_eyes.py {output_dir}")
if __name__ == "__main__":
main()
+98
View File
@@ -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()
+131
View File
@@ -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()
+210
View File
@@ -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()
+133
View File
@@ -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()
+100
View File
@@ -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()
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Import previously saved eye images into the patch library.
This script reads eye images from a directory (along with metadata.json)
and imports them into the patch library database.
Usage:
python scripts/import_saved_eyes.py /path/to/saved/eyes
This script must be run with backend dependencies available
(e.g., inside the backend container).
"""
import os
import sys
import json
import argparse
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "backend"))
from PIL import Image
from sqlalchemy.orm import Session
from app.database import SessionLocal, engine, Base
from app.models.patch import Patch
from app.services.patch_library import PatchLibraryService
def import_from_directory(source_dir: Path, db: Session, patch_service: PatchLibraryService) -> int:
"""
Import eye images from a directory.
Args:
source_dir: Directory containing eye images and metadata.json
db: Database session
patch_service: PatchLibraryService instance
Returns:
Number of successfully imported images
"""
metadata_path = source_dir / "metadata.json"
if not metadata_path.exists():
print(f"Error: metadata.json not found in {source_dir}")
print("Scanning for PNG files instead...")
# Fallback: import all PNG files with default metadata
png_files = list(source_dir.glob("*.png"))
metadata = []
for png_file in png_files:
img = Image.open(png_file)
metadata.append({
'filename': png_file.name,
'name': png_file.stem.replace('_', ' ').title(),
'description': f'Imported eye image: {png_file.name}',
'tags': 'eye,imported',
'width': img.width,
'height': img.height
})
else:
with open(metadata_path, 'r') as f:
metadata = json.load(f)
imported = 0
for item in metadata:
try:
filename = item['filename']
filepath = source_dir / filename
if not filepath.exists():
print(f" Skipping (file not found): {filename}")
continue
name = item['name']
description = item.get('description', '')
tags = item.get('tags', 'eye,imported')
# Check if already exists
existing = db.query(Patch).filter(Patch.name == name).first()
if existing:
print(f" Skipping (exists): {name}")
continue
# Load image
img = Image.open(filepath)
# Create database record
patch = Patch(
name=name,
description=description,
source_type="imported",
width=img.width,
height=img.height,
tags=tags,
category="eye",
is_public=True,
file_path="",
thumbnail_path=""
)
db.add(patch)
db.flush()
# Save to patch library
patch_path = patch_service.get_patch_path(patch.id)
img.save(patch_path, 'PNG')
# Create thumbnail
thumb_path = patch_service.get_thumbnail_path(patch.id)
patch_service.create_thumbnail(patch_path, thumb_path)
# Update paths
patch.file_path = f"patch_library/{patch.id}.png"
patch.thumbnail_path = f"patch_library/{patch.id}_thumb.png"
db.commit()
imported += 1
print(f" Imported: {name} (ID: {patch.id})")
except Exception as e:
db.rollback()
print(f" Error importing {item.get('filename', 'unknown')}: {e}")
return imported
def main():
parser = argparse.ArgumentParser(description='Import saved eye images to patch library')
parser.add_argument('source_dir', help='Directory containing eye images and metadata.json')
args = parser.parse_args()
source_dir = Path(args.source_dir)
if not source_dir.exists():
print(f"Error: Directory not found: {source_dir}")
sys.exit(1)
print("=" * 60)
print("Eye Image Importer")
print("=" * 60)
# Initialize database
print("\nInitializing database...")
Base.metadata.create_all(bind=engine)
db = SessionLocal()
# Initialize patch library
data_dir = os.environ.get("DATA_DIR", str(Path(__file__).parent.parent / "data"))
patch_service = PatchLibraryService(data_dir)
print(f"Source directory: {source_dir}")
print(f"Patch library: {patch_service.patch_library_dir}")
# Import
print("\nImporting eyes...")
imported = import_from_directory(source_dir, db, patch_service)
print("\n" + "=" * 60)
print(f"Import Complete! Imported {imported} eyes.")
print("=" * 60)
db.close()
if __name__ == "__main__":
main()
+45
View File
@@ -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()
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
Startup script to import classic eyes into the patch library.
This should be run once when the backend starts, or manually.
Usage:
python scripts/startup_import_eyes.py
This script:
1. Converts PPM files to PNG (if needed)
2. Imports all classic eyes into the patch library database
"""
import os
import sys
import json
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "backend"))
try:
from PIL import Image
from sqlalchemy.orm import Session
from app.database import SessionLocal, engine, Base
from app.models.patch import Patch
from app.services.patch_library import PatchLibraryService
except ImportError as e:
print(f"Error: Required modules not available: {e}")
print("Run this script inside the backend container.")
sys.exit(1)
def convert_ppm_to_png(ppm_dir: Path) -> int:
"""Convert all PPM files to PNG in the given directory."""
converted = 0
for ppm_file in ppm_dir.glob("*.ppm"):
png_file = ppm_file.with_suffix('.png')
if not png_file.exists():
try:
img = Image.open(ppm_file)
img.save(png_file, 'PNG')
converted += 1
print(f" Converted: {ppm_file.name} -> {png_file.name}")
except Exception as e:
print(f" Error converting {ppm_file.name}: {e}")
return converted
def import_eyes(source_dir: Path, db: Session, patch_service: PatchLibraryService) -> int:
"""Import eye images from the given directory."""
metadata_path = source_dir / "metadata.json"
if not metadata_path.exists():
print(f" No metadata.json found in {source_dir}")
return 0
with open(metadata_path, 'r') as f:
metadata = json.load(f)
imported = 0
for item in metadata:
try:
# Try PNG first, then PPM
filename = item.get('filename', item.get('ppm_filename', '')).replace('.ppm', '.png')
filepath = source_dir / filename
if not filepath.exists():
# Try PPM
filepath = source_dir / item.get('ppm_filename', filename.replace('.png', '.ppm'))
if not filepath.exists():
continue
name = item['name']
# Check if already exists
existing = db.query(Patch).filter(Patch.name == name).first()
if existing:
continue # Silent skip for duplicates on startup
# Load and convert if PPM
img = Image.open(filepath).convert('RGBA')
# Create database record
patch = Patch(
name=name,
description=item.get('description', ''),
source_type="imported",
width=img.width,
height=img.height,
tags=item.get('tags', 'eye,classic'),
category="eye",
is_public=True,
file_path="",
thumbnail_path=""
)
db.add(patch)
db.flush()
# Save to patch library as PNG
patch_path = patch_service.get_patch_path(patch.id)
img.save(patch_path, 'PNG')
# Create thumbnail
thumb_path = patch_service.get_thumbnail_path(patch.id)
patch_service.create_thumbnail(patch_path, thumb_path)
# Update paths
patch.file_path = f"patch_library/{patch.id}.png"
patch.thumbnail_path = f"patch_library/{patch.id}_thumb.png"
db.commit()
imported += 1
print(f" Imported: {name} (ID: {patch.id})")
except Exception as e:
db.rollback()
print(f" Error: {e}")
return imported
def main():
print("=" * 60)
print("Classic Eyes Startup Import")
print("=" * 60)
# Find the classic eyes directory
data_dir = Path(os.environ.get("DATA_DIR", "/app/data"))
if not data_dir.exists():
data_dir = Path(__file__).parent.parent / "data"
ppm_dir = data_dir / "classic_eyes_ppm"
if not ppm_dir.exists():
print(f"\nNo classic eyes found at: {ppm_dir}")
print("Run generate_classic_eyes_ppm.py first.")
return
# Initialize database
print("\nInitializing database...")
Base.metadata.create_all(bind=engine)
db = SessionLocal()
# Initialize patch library
patch_service = PatchLibraryService(str(data_dir))
# Convert PPM to PNG
print("\nConverting PPM to PNG (if needed)...")
converted = convert_ppm_to_png(ppm_dir)
if converted > 0:
print(f" Converted {converted} files")
else:
print(" All files already converted")
# Import eyes
print("\nImporting classic eyes...")
imported = import_eyes(ppm_dir, db, patch_service)
# Count existing
total = db.query(Patch).filter(Patch.category == "eye").count()
print("\n" + "=" * 60)
print(f"Import Complete!")
print(f" Newly imported: {imported}")
print(f" Total eye patches in library: {total}")
print("=" * 60)
db.close()
if __name__ == "__main__":
main()