File size: 2,787 Bytes
855b5c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
"""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)))