forge/backend/app/services/background_remover.py

143 lines
5.2 KiB
Python

"""Background Remover Service - Clipping Magic API"""
import httpx
import os
import base64
from uuid import uuid4
from datetime import datetime
from app.database import SessionLocal
from app.models.job import Job
from app.models.asset import Asset
from app.config import settings
async def remove_background(job_id: str):
"""Remove background from image using Clipping Magic"""
db = SessionLocal()
try:
job = db.query(Job).filter(Job.id == job_id).first()
if not job:
return
input_data = job.input_data
input_asset_ids = job.input_asset_ids
if not input_asset_ids:
raise ValueError("No input asset provided")
input_asset = db.query(Asset).filter(Asset.id == input_asset_ids[0]).first()
if not input_asset:
raise ValueError("Input asset not found")
job.progress = 10
job.api_provider = "clipping_magic"
db.commit()
# Read input image
with open(input_asset.file_path, "rb") as f:
image_data = f.read()
output_format = input_data.get("output_format", "png")
job.progress = 20
db.commit()
# Call Clipping Magic API
async with httpx.AsyncClient(timeout=120) as client:
# Use API ID and Secret for HTTP Basic Auth
api_id = settings.clipping_magic_api_id or settings.clipping_magic_api_key
api_secret = settings.clipping_magic_api_secret or ""
response = await client.post(
"https://clippingmagic.com/api/v1/images",
auth=(api_id, api_secret),
files={"image": (input_asset.original_filename, image_data, input_asset.mime_type)},
data={
"format": "clipping_path_tiff" if output_format == "tiff" else ("result" if output_format == "png" else "result")
}
)
response.raise_for_status()
content_type = response.headers.get("content-type", "")
if "application/json" in content_type:
# Flow 1: API returns JSON with image ID (async processing or default)
result = response.json()
image_id = result.get("image", {}).get("id")
job.progress = 50
db.commit()
if image_id:
# Download the result
download_response = await client.get(
f"https://clippingmagic.com/api/v1/images/{image_id}",
auth=(api_id, api_secret),
params={"format": "clipping_path_tiff" if output_format == "tiff" else "result"}
)
download_response.raise_for_status()
processed_data = download_response.content
else:
# Flow 2: API returns the image directly (synchronous processing requested via format='result')
processed_data = response.content
job.progress = 70
db.commit()
job.progress = 80
db.commit()
# Save output
ext = "tiff" if output_format == "tiff" else ("png" if output_format == "png" else "webp")
filename = f"nobg_{uuid4()}.{ext}"
storage_path = os.path.join(settings.storage_path, "images")
os.makedirs(storage_path, exist_ok=True)
file_path = os.path.join(storage_path, filename)
with open(file_path, "wb") as f:
f.write(processed_data)
# Create output asset
output_asset = Asset(
user_id=job.user_id,
project_id=job.project_id,
original_filename=filename,
stored_filename=filename,
file_path=file_path,
file_type="image",
mime_type=f"image/{ext}",
file_size_bytes=len(processed_data),
width=input_asset.width,
height=input_asset.height,
source_module="background_remover",
source_job_id=job.id,
parent_asset_id=input_asset.id,
asset_metadata={"output_format": output_format}
)
db.add(output_asset)
db.commit()
db.refresh(output_asset)
job.output_asset_ids = [output_asset.id]
job.output_data = {"asset_id": str(output_asset.id), "file_path": file_path}
# Delete from Clipping Magic if we have an image_id (Only needed for Flow 1)
if "application/json" in content_type and 'image_id' in locals() and image_id:
try:
await client.post(
f"https://clippingmagic.com/api/v1/images/{image_id}/delete",
auth=(api_id, api_secret)
)
except Exception as e:
pass # Ignore cleanup errors
job.progress = 100
job.status = "completed"
job.completed_at = datetime.utcnow()
db.commit()
except Exception as e:
job.status = "failed"
job.error_message = str(e)
db.commit()
finally:
db.close()