ProductShowcaseStudio / backend /api /image_service.py
sushilideaclan01's picture
change the
c4f9173
Raw
History Blame Contribute Delete
5.11 kB
"""
Image Service API endpoints
Handles image compression, storage, and serving
"""
import base64
import re
from datetime import datetime
from pathlib import Path
import httpx
from fastapi import APIRouter, HTTPException, Response, UploadFile, File, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from utils.public_url import get_public_base_url
from utils.storage import temp_images
from utils.image_processor import compress_and_store_image
router = APIRouter()
IMAGE_STORAGE_DIR = Path("storage/images")
# High-quality settings for reference/continuity frames (last frame of previous segment)
REFERENCE_FRAME_QUALITY = 92
REFERENCE_FRAME_MAX_WIDTH = 1920
REFERENCE_FRAME_MAX_HEIGHT = 1080
def _is_reference_frame_filename(filename: str) -> bool:
if not filename:
return False
name = filename.lower()
return bool(re.match(r"^(frame-|last-frame\.|whisper-frame-)", name) or "frame" in name and name.endswith((".jpg", ".jpeg", ".png")))
@router.post("/upload-image")
async def upload_image(
file: UploadFile = File(...),
reference: bool = Query(False, description="High quality for last-frame/reference uploads"),
):
"""
Upload and host an image, returns public URL.
Use ?reference=true when uploading a continuity/reference frame (last frame of previous segment)
for higher quality and less downscaling.
"""
try:
image_bytes = await file.read()
encoded = base64.b64encode(image_bytes).decode('utf-8')
data_url = f"data:{file.content_type or 'image/jpeg'};base64,{encoded}"
public_url = get_public_base_url()
use_high_quality = reference or _is_reference_frame_filename(file.filename or "")
if use_high_quality:
hosted_url = await compress_and_store_image(
data_url, public_url,
max_width=REFERENCE_FRAME_MAX_WIDTH,
max_height=REFERENCE_FRAME_MAX_HEIGHT,
quality=REFERENCE_FRAME_QUALITY,
)
else:
hosted_url = await compress_and_store_image(data_url, public_url)
return JSONResponse(content={
"url": hosted_url,
"filename": file.filename,
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Image upload failed: {str(e)}")
class HostImageUrlBody(BaseModel):
url: str = Field(..., min_length=8, description="Public HTTPS image URL (e.g. CDN)")
@router.post("/host-image-url")
async def host_image_from_url(body: HostImageUrlBody):
"""
Download an image server-side and host it like /upload-image (reference quality).
Used after scraping so KIE and the planner can use a stable public URL.
"""
url = body.url.strip()
try:
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
r = await client.get(url, headers={"User-Agent": "ProductShowcase/1.0"})
r.raise_for_status()
content = r.content
ct = (r.headers.get("content-type") or "image/jpeg").split(";")[0].strip() or "image/jpeg"
if not ct.startswith("image/"):
raise HTTPException(
status_code=400,
detail=f"URL did not return an image (content-type: {ct})",
)
except httpx.HTTPError as e:
raise HTTPException(status_code=502, detail=f"Could not download image: {e}")
try:
b64 = base64.b64encode(content).decode("utf-8")
data_url = f"data:{ct};base64,{b64}"
public_url = get_public_base_url()
hosted_url = await compress_and_store_image(
data_url,
public_url,
max_width=REFERENCE_FRAME_MAX_WIDTH,
max_height=REFERENCE_FRAME_MAX_HEIGHT,
quality=REFERENCE_FRAME_QUALITY,
)
return JSONResponse(content={"url": hosted_url, "source_url": url})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Host image failed: {str(e)}")
@router.get("/images/{image_id}")
async def serve_image(image_id: str):
"""
Serve temporarily stored images.
Prefer in-memory cache; fallback to disk for process restarts.
"""
image_data = temp_images.get(image_id)
if image_data is None:
disk_path = IMAGE_STORAGE_DIR / f"{image_id}.jpg"
if disk_path.exists():
raw = disk_path.read_bytes()
image_data = {
"buffer": raw,
"content_type": "image/jpeg",
}
# Warm memory cache for subsequent requests in this process.
temp_images[image_id] = {
"buffer": raw,
"timestamp": datetime.now().timestamp(),
"content_type": "image/jpeg",
}
else:
raise HTTPException(status_code=404, detail="Image not found")
return Response(
content=image_data['buffer'],
media_type=image_data['content_type'],
headers={
'Cache-Control': 'public, max-age=3600'
}
)