local-in-30s / api_bridge.py
Jackylau666's picture
Reduce ZeroGPU decode cost and restore same-origin API calls
d792c3e verified
Raw
History Blame Contribute Delete
4.43 kB
import base64
from io import BytesIO
from pathlib import Path
from typing import Any, Callable
from PIL import Image
DecodeRunner = Callable[[Image.Image], str]
SectionParser = Callable[[str], dict[str, Any]]
DecodeBuilder = Callable[..., dict[str, Any]]
class DecodePayloadError(ValueError):
pass
MAX_DECODE_IMAGE_EDGE = 640
def _strip_data_url(value: str) -> str:
if "," in value and value.lstrip().startswith("data:"):
return value.split(",", 1)[1]
return value
def _image_from_payload(payload: dict[str, Any]) -> tuple[Image.Image, str, str]:
image_b64 = payload.get("image")
if not isinstance(image_b64, str) or not image_b64.strip():
raise DecodePayloadError("Missing base64 image.")
normalized_b64 = _strip_data_url(image_b64.strip())
mime = payload.get("mime")
if not isinstance(mime, str) or not mime.startswith("image/"):
mime = "image/jpeg"
try:
raw = base64.b64decode(normalized_b64, validate=True)
image = Image.open(BytesIO(raw)).convert("RGB")
image.thumbnail((MAX_DECODE_IMAGE_EDGE, MAX_DECODE_IMAGE_EDGE), Image.Resampling.LANCZOS)
except Exception as exc:
raise DecodePayloadError("Invalid image payload.") from exc
return image, normalized_b64, mime
def decode_payload_to_culture_decode(
payload: dict[str, Any],
*,
run_decode: DecodeRunner,
parse_sections: SectionParser,
sections_to_decode: DecodeBuilder,
) -> dict[str, Any]:
image, image_b64, mime = _image_from_payload(payload)
raw = run_decode(image)
sections = parse_sections(raw)
decode = dict(sections_to_decode(sections, uploaded_b64=image_b64))
decode["image"] = f"{mime};base64,{image_b64}" if mime.startswith("data:") else f"data:{mime};base64,{image_b64}"
decode.pop("_uploaded_b64", None)
return decode
def create_hackathon_app(
gradio_demo: Any,
decode_payload: Callable[[dict[str, Any]], dict[str, Any]],
narrate_payload: Callable[[dict[str, Any]], dict[str, Any]] | None = None,
*,
static_dir: str = "dist",
) -> Any:
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
import gradio as gr
app = FastAPI(title="Local in 30s Hackathon API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/health")
async def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/api/decode")
async def decode_api(payload: dict[str, Any]) -> dict[str, Any]:
try:
return decode_payload(payload)
except DecodePayloadError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"AI decode failed: {exc}") from exc
@app.post("/api/narrate")
async def narrate_api(payload: dict[str, Any]) -> dict[str, Any]:
if narrate_payload is None:
return {"audio": None, "usedFallback": True}
try:
return narrate_payload(payload)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"AI narration failed: {exc}") from exc
dist = Path(static_dir)
if dist.exists():
assets = dist / "assets"
if assets.exists():
app.mount("/ui/assets", StaticFiles(directory=str(assets)), name="ui-assets")
@app.get("/")
async def react_home() -> FileResponse:
standalone = dist / "standalone-preview.html"
if standalone.exists():
return FileResponse(str(standalone))
return FileResponse(str(dist / "index.html"))
@app.get("/ui")
@app.get("/ui/")
@app.get("/ui/{path:path}")
async def react_ui(path: str = "") -> FileResponse:
target = dist / path
if path and target.exists() and target.is_file():
return FileResponse(str(target))
return FileResponse(str(dist / "index.html"))
return gr.mount_gradio_app(app, gradio_demo, path="/gradio")