Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 40 additions & 25 deletions apps/web/backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response, JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from PIL import Image
from PIL import Image, ImageOps
import pillow_heif # Required for HEIF/HEIC decoding
import uvicorn


# Import withoutbg package (install via: uv sync or pip install -e ../../../packages/python)
from withoutbg import WithoutBG, __version__
from withoutbg.exceptions import WithoutBGError
from withoutbg.api import ProAPI

# Enable HEIF support globally for Pillow
pillow_heif.register_heif_opener()

app = FastAPI(
title="withoutbg API",
description="AI-powered background removal API",
Expand Down Expand Up @@ -56,7 +61,7 @@ async def health_check():
"status": "healthy",
"version": __version__,
"service": "withoutbg-api",
"models_loaded": _model is not None
"models_loaded": _model is not None,
}


Expand All @@ -70,24 +75,36 @@ async def remove_background_endpoint(
"""
Remove background from a single image.

Supports standard formats (PNG, JPG, WebP) and native Apple HEIC/HEIF files.
Automatically handles EXIF orientation to ensure upright output.

Args:
file: Image file to process
format: Output format (png, jpg, webp)
quality: Quality for JPEG output (1-100)
api_key: Optional API key for cloud processing

Returns:
Processed image with background removed
"""
try:
# Validate file type
if not file.content_type or not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="File must be an image")

# Support standard image types and native Apple HEIC/HEIF
is_image = file.content_type and file.content_type.startswith("image/")
is_heic = file.filename and file.filename.lower().endswith((".heic", ".heif"))

if not (is_image or is_heic):
raise HTTPException(
status_code=400, detail="File must be an image (JPEG, PNG, or HEIC)"
)

# Read uploaded file
contents = await file.read()
input_image = Image.open(io.BytesIO(contents))

raw_image = Image.open(io.BytesIO(contents))

# 1. Apply EXIF orientation (prevents rotated mobile uploads)
# 2. Force RGBA for consistency across inference models
input_image = ImageOps.exif_transpose(raw_image).convert("RGBA")

# Process image using appropriate model
if api_key:
# Use API for this specific request
Expand All @@ -98,13 +115,13 @@ async def remove_background_endpoint(
if _model is None:
raise HTTPException(
status_code=503,
detail="Models not loaded. Server may still be starting up."
detail="Models not loaded. Server may still be starting up.",
)
result = _model.remove_background(input_image)

# Convert result to bytes
output_buffer = io.BytesIO()

# Handle format conversion
if format.lower() in ["jpg", "jpeg"]:
# Convert RGBA to RGB for JPEG
Expand All @@ -121,17 +138,15 @@ async def remove_background_endpoint(
else: # PNG
result.save(output_buffer, format="PNG")
media_type = "image/png"

output_buffer.seek(0)

return Response(
content=output_buffer.getvalue(),
media_type=media_type,
headers={
"Content-Disposition": f"inline; filename=withoutbg.{format}"
}
headers={"Content-Disposition": f"inline; filename=withoutbg.{format}"},
)

except WithoutBGError as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
Expand All @@ -142,10 +157,10 @@ async def remove_background_endpoint(
async def get_usage_endpoint(api_key: str):
"""
Get API usage statistics.

Args:
api_key: API key for cloud service

Returns:
Usage statistics
"""
Expand All @@ -161,7 +176,7 @@ async def get_usage_endpoint(api_key: str):
if STATIC_DIR.exists():
# Serve static assets (js, css, images, etc.)
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")

# Root route - serve index.html
@app.get("/")
async def root():
Expand All @@ -170,25 +185,25 @@ async def root():
if index_path.exists():
return FileResponse(index_path)
raise HTTPException(status_code=404, detail="Frontend not found")

# Catch-all route for React SPA - must be last
@app.get("/{full_path:path}")
async def serve_frontend(full_path: str):
"""Serve the React frontend for all non-API routes."""
# Don't serve frontend for API routes
if full_path.startswith("api/"):
raise HTTPException(status_code=404, detail="API endpoint not found")

# Try to serve the requested file
file_path = STATIC_DIR / full_path
if file_path.is_file():
return FileResponse(file_path)

# Otherwise, serve index.html (SPA routing)
index_path = STATIC_DIR / "index.html"
if index_path.exists():
return FileResponse(index_path)

raise HTTPException(status_code=404, detail="Not found")


Expand Down
4 changes: 4 additions & 0 deletions apps/web/backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies = [
"uvicorn[standard]>=0.32.0",
"python-multipart>=0.0.12",
"pillow>=8.0.0",
"pillow-heif>=1.3.0",
]

[build-system]
Expand All @@ -17,3 +18,6 @@ build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["app"]

[tool.uv.sources]
withoutbg = { path = "../../../packages/python", editable = true }
85 changes: 77 additions & 8 deletions apps/web/backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading