| """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))) |
|
|