"""Hallmark — HTTP surface. Three things a user can do: POST /api/mint generate an image and hallmark it into B2 POST /api/verify upload any file, ask where it came from GET /api/library browse what this instance has minted """ from __future__ import annotations import os from pathlib import Path from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, Field from hallmark import __version__, studio app = FastAPI(title="Hallmark", version=__version__) STATIC = Path(__file__).parent / "static" MAX_UPLOAD_BYTES = 25 * 1024 * 1024 class MintRequest(BaseModel): prompt: str = Field(min_length=3, max_length=1200) model: str = "flux" width: int = Field(default=1024, ge=256, le=1536) height: int = Field(default=1024, ge=256, le=1536) seed: int | None = None @app.get("/api/health") def health() -> dict[str, object]: return { "ok": True, "version": __version__, "b2_configured": studio.b2_configured(), "bucket": studio.BUCKET, "region": studio.REGION, } @app.post("/api/mint") def mint(req: MintRequest) -> JSONResponse: try: result = studio.mint( req.prompt, model=req.model, width=req.width, height=req.height, seed=req.seed ) except studio.NotConfigured as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc except Exception as exc: raise HTTPException(status_code=502, detail=f"generation failed: {exc}") from exc return JSONResponse(result.as_dict()) @app.post("/api/verify") async def verify(file: UploadFile = File(...)) -> JSONResponse: data = await file.read(MAX_UPLOAD_BYTES + 1) if len(data) > MAX_UPLOAD_BYTES: raise HTTPException(status_code=413, detail="file larger than 25 MB") if not data: raise HTTPException(status_code=400, detail="empty upload") try: report = studio.verify_bytes(data, filename=file.filename or "upload") except studio.NotConfigured as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc return JSONResponse(report) @app.get("/api/library") def library(limit: int = 60) -> JSONResponse: try: return JSONResponse({"items": studio.library(limit=limit)}) except studio.NotConfigured as exc: raise HTTPException(status_code=503, detail=str(exc)) from exc @app.get("/") def index() -> FileResponse: return FileResponse(STATIC / "index.html") app.mount("/static", StaticFiles(directory=STATIC), name="static") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))