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
145 lines
4.1 KiB
Python
145 lines
4.1 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
|
|
from sqlalchemy.orm import Session
|
|
from typing import List
|
|
import shutil
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
|
from app.database import get_db
|
|
from app.models.project import Project
|
|
from app.models.edit import Edit
|
|
from app.schemas import ProjectCreate, ProjectResponse, EditResponse, UploadResponse
|
|
from app.services.edit_service import EditService
|
|
from app.config import settings
|
|
|
|
router = APIRouter(prefix="/projects", tags=["projects"])
|
|
|
|
|
|
@router.post("/", response_model=ProjectResponse)
|
|
def create_project(
|
|
project: ProjectCreate,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Create a new project"""
|
|
# For MVP, we'll use a default user_id of 1
|
|
# In production, this would come from authentication
|
|
user_id = 1
|
|
|
|
db_project = Project(
|
|
user_id=user_id,
|
|
name=project.name
|
|
)
|
|
db.add(db_project)
|
|
db.commit()
|
|
db.refresh(db_project)
|
|
|
|
# Create project directory
|
|
edit_service = EditService()
|
|
edit_service.ensure_project_dir(db_project.id)
|
|
|
|
return db_project
|
|
|
|
|
|
@router.get("/", response_model=List[ProjectResponse])
|
|
def list_projects(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""List all projects"""
|
|
projects = db.query(Project).offset(skip).limit(limit).all()
|
|
return projects
|
|
|
|
|
|
@router.get("/{project_id}", response_model=ProjectResponse)
|
|
def get_project(
|
|
project_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get a specific project"""
|
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
return project
|
|
|
|
|
|
@router.delete("/{project_id}")
|
|
def delete_project(
|
|
project_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Delete a project"""
|
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
|
|
# Delete project directory
|
|
edit_service = EditService()
|
|
project_dir = edit_service.get_project_dir(project_id)
|
|
if project_dir.exists():
|
|
shutil.rmtree(project_dir)
|
|
|
|
db.delete(project)
|
|
db.commit()
|
|
|
|
return {"status": "success", "message": f"Project {project_id} deleted"}
|
|
|
|
|
|
@router.post("/{project_id}/upload", response_model=UploadResponse)
|
|
async def upload_image(
|
|
project_id: int,
|
|
file: UploadFile = File(...),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Upload an image to a project"""
|
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
|
|
# Validate file type
|
|
if not file.content_type.startswith('image/'):
|
|
raise HTTPException(status_code=400, detail="File must be an image")
|
|
|
|
# Create project directory
|
|
edit_service = EditService()
|
|
edit_service.ensure_project_dir(project_id)
|
|
|
|
# Save original and current images
|
|
original_path = edit_service.get_original_image_path(project_id)
|
|
current_path = edit_service.get_current_image_path(project_id)
|
|
|
|
# Read and validate image
|
|
contents = await file.read()
|
|
try:
|
|
image = Image.open(BytesIO(contents))
|
|
image = image.convert('RGBA')
|
|
|
|
# Save images
|
|
image.save(original_path, 'PNG')
|
|
image.save(current_path, 'PNG')
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}")
|
|
|
|
return UploadResponse(
|
|
project_id=project_id,
|
|
original_url=f"/projects/{project_id}/original",
|
|
current_url=f"/projects/{project_id}/current"
|
|
)
|
|
|
|
|
|
@router.get("/{project_id}/edits", response_model=List[EditResponse])
|
|
def list_edits(
|
|
project_id: int,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""List all edits for a project"""
|
|
project = db.query(Project).filter(Project.id == project_id).first()
|
|
if not project:
|
|
raise HTTPException(status_code=404, detail="Project not found")
|
|
|
|
edits = db.query(Edit).filter(Edit.project_id == project_id).order_by(Edit.created_at.desc()).all()
|
|
return edits
|
|
|
|
|
|
from io import BytesIO
|