Files
PaintPlus/backend/app/main.py
T
Claude 3746c02d44 Add SAM click-to-select with brush refinement in AI Edit tool
Backend:
- sam_service.py: auto-downloads SAM ViT-B (~375 MB) on first use with
  progress tracking; loads model to CUDA/MPS/CPU; predict_points() takes
  multi-point prompts (include/exclude labels) and returns best mask
- POST /api/segment/point: SAM point-prompt endpoint; returns mask PNG
- GET  /api/segment/install-status: poll download progress
- POST /api/segment/install: explicit trigger (also auto on first click)
- main.py: pre-download SAM on startup alongside NCNN

Frontend (ai_edit.js):
- Click mode (default): click object → SAM generates mask instantly
  Alt+click → subtract (deselect over-selected area)
  Multiple clicks accumulate for multi-object or refinement
- Brush + / Brush − modes: paint to add or erase from SAM mask by hand
- If SAM model is still downloading on first click: inline progress bar,
  user retries the click when done
- Unified action bar: Erase | Replace (inline prompt) | Upscale | Expand | Clear
- All modes share the same mask canvas; SAM and brush are fully composited

https://claude.ai/code/session_01B58MaJCU1R6KwBDJCp8AfN
2026-06-10 18:18:33 +00:00

123 lines
3.8 KiB
Python

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, HTMLResponse
from contextlib import asynccontextmanager
from pathlib import Path
import asyncio
import os
from app.config import settings
from app.database import init_db
from app.routers import projects, edits, images, patches, generate, tools, ai_tools, print_tools
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize database on startup; auto-install Real-ESRGAN NCNN in background."""
init_db()
# Kick off NCNN install in background if no AI upscaler detected
from app.services.upscale import probe_upscale_capabilities, ensure_ncnn_installed
caps = probe_upscale_capabilities()
if not caps["realesrgan_pytorch"] and not caps["realesrgan_ncnn"]:
asyncio.create_task(ensure_ncnn_installed())
# Pre-download SAM model in background so first click is fast
from app.services.sam_service import ensure_sam_installed
asyncio.create_task(ensure_sam_installed())
yield
app = FastAPI(
title="AI Photo Edit API",
description="API for AI-powered photo editing with mask-scoped regeneration",
version="1.0.0",
lifespan=lifespan
)
# Configure CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins_list,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(projects.router)
app.include_router(edits.router)
app.include_router(images.router)
app.include_router(patches.router)
app.include_router(generate.router)
app.include_router(tools.router)
app.include_router(ai_tools.router)
app.include_router(print_tools.router)
@app.get("/api")
def api_root():
"""API info endpoint"""
return {
"name": "AI Photo Edit API",
"version": "1.0.0",
"status": "running"
}
@app.get("/health")
def health():
"""Health check endpoint"""
return {"status": "healthy"}
# Static files directory
STATIC_DIR = Path("/app/static")
# Serve static assets - mount subdirectories if they exist
if STATIC_DIR.exists():
# React-style assets folder
if (STATIC_DIR / "assets").exists():
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
# miniPaint dist folder (webpack bundle)
if (STATIC_DIR / "dist").exists():
app.mount("/dist", StaticFiles(directory=STATIC_DIR / "dist"), name="dist")
# miniPaint images folder
if (STATIC_DIR / "images").exists():
app.mount("/images", StaticFiles(directory=STATIC_DIR / "images"), name="images")
# miniPaint CSS folder
if (STATIC_DIR / "src").exists():
app.mount("/src", StaticFiles(directory=STATIC_DIR / "src"), name="src")
@app.get("/", response_class=HTMLResponse)
async def serve_spa():
"""Serve miniPaint index.html"""
index_path = STATIC_DIR / "index.html"
if index_path.exists():
return FileResponse(index_path)
return HTMLResponse("<h1>Frontend not built. Run npm build in frontend/</h1>")
@app.get("/{full_path:path}")
async def serve_spa_routes(request: Request, full_path: str):
"""
Catch-all route for serving static files.
Serves static files if they exist, otherwise returns index.html.
"""
# Don't catch API routes
if full_path.startswith(("projects", "edits", "patches", "tools", "generate", "health", "docs", "openapi.json", "api")):
return {"detail": "Not Found"}
# Check if it's a static file
static_file = STATIC_DIR / full_path
if static_file.exists() and static_file.is_file():
return FileResponse(static_file)
# Otherwise serve index.html
index_path = STATIC_DIR / "index.html"
if index_path.exists():
return FileResponse(index_path)
return HTMLResponse("<h1>Frontend not built</h1>", status_code=404)