Files
face-lock/app/main.py

181 lines
7.2 KiB
Python

from io import BytesIO
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import HTMLResponse, StreamingResponse
from app.config import settings
app = FastAPI(title="face-lock", version="0.3.0")
def require_auth(request: Request) -> None:
if not settings.auth_enabled:
return
header_name = settings.auth_header_name.lower()
provided = request.headers.get(header_name)
if not provided and request.headers.get("authorization", "").lower().startswith("bearer "):
provided = request.headers.get("authorization", "")[7:].strip()
if provided != settings.auth_token:
raise HTTPException(status_code=401, detail="unauthorized")
@app.get("/health")
def health():
return {
"ok": True,
"env": settings.env,
"auth_enabled": settings.auth_enabled,
"auth_header": settings.auth_header_name if settings.auth_enabled else None,
}
@app.get("/", response_class=HTMLResponse)
def index():
return HTMLResponse(
"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://cdn.tailwindcss.com"></script>
<title>face-lock</title>
</head>
<body class="bg-slate-950 text-slate-100 min-h-screen">
<main class="mx-auto max-w-6xl p-6">
<div class="mb-6 flex items-center justify-between gap-4">
<div>
<h1 class="text-3xl font-bold">face-lock</h1>
<p class="text-slate-400">Square the subject, crop it, and keep the raw blobs out of sight.</p>
</div>
<a class="rounded-lg border border-slate-700 px-3 py-2 text-sm text-cyan-300 hover:bg-slate-900" href="/docs" target="_blank">Docs</a>
</div>
<div class="grid gap-6 md:grid-cols-2">
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-4">
<label class="block text-sm text-slate-400">Image</label>
<input id="file" type="file" accept="image/*" class="mt-2 block w-full rounded-lg border border-slate-700 bg-slate-950 p-3" />
<div class="mt-4 grid gap-4 sm:grid-cols-2">
<div>
<label class="block text-sm text-slate-400">Detector</label>
<select id="detector" class="mt-2 block w-full rounded-lg border border-slate-700 bg-slate-950 p-3">
<option value="face">Face</option>
<option value="animal">Animal</option>
<option value="person">Person</option>
<option value="subject" selected>Subject</option>
</select>
</div>
<div>
<label class="block text-sm text-slate-400">Buffer ratio</label>
<input id="buffer_ratio" type="number" step="0.05" min="0" max="0.6" value="0.20" class="mt-2 block w-full rounded-lg border border-slate-700 bg-slate-950 p-3" />
</div>
</div>
<div class="mt-4">
<label class="block text-sm text-slate-400">Auth token (only if enabled)</label>
<input id="auth_token" type="password" placeholder="paste token here" class="mt-2 block w-full rounded-lg border border-slate-700 bg-slate-950 p-3" />
</div>
<button id="go" class="mt-4 rounded-lg bg-cyan-500 px-4 py-2 font-semibold text-slate-950">Process</button>
<pre id="meta" class="mt-4 whitespace-pre-wrap rounded-lg bg-slate-950 p-3 text-xs text-slate-300"></pre>
</section>
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-4">
<div class="mb-3 text-sm font-semibold text-slate-400">Result</div>
<div class="grid gap-4">
<div>
<div class="mb-2 text-xs uppercase tracking-wide text-slate-500">Crop</div>
<img id="crop" class="hidden w-full rounded-xl border border-slate-800" />
</div>
<div>
<div class="mb-2 text-xs uppercase tracking-wide text-slate-500">Annotated source</div>
<img id="annotated" class="hidden w-full rounded-xl border border-slate-800" />
</div>
</div>
</section>
</div>
</main>
<script>
const file = document.getElementById('file');
const go = document.getElementById('go');
const crop = document.getElementById('crop');
const annotated = document.getElementById('annotated');
const meta = document.getElementById('meta');
go.onclick = async () => {
if (!file.files.length) return;
const form = new FormData();
form.append('file', file.files[0]);
form.append('detector', document.getElementById('detector').value);
form.append('buffer_ratio', document.getElementById('buffer_ratio').value);
meta.textContent = 'Working...';
const headers = {};
const token = document.getElementById('auth_token').value.trim();
if (token) headers['__AUTH_HEADER_NAME__'] = token;
const resp = await fetch('/api/focus', { method: 'POST', body: form, headers });
const data = await resp.json();
if (!resp.ok) {
meta.textContent = JSON.stringify(data, null, 2);
return;
}
meta.textContent = JSON.stringify({
filename: data.filename,
detector: data.detector,
method: data.method,
buffer_ratio: data.buffer_ratio,
detected_bbox: data.detected_bbox,
square_bbox: data.square_bbox,
source_size: data.source_size,
}, null, 2);
crop.src = data.crop_data_url;
annotated.src = data.annotated_data_url;
crop.classList.remove('hidden');
annotated.classList.remove('hidden');
};
</script>
</body>
</html>
""".replace("__AUTH_HEADER_NAME__", settings.auth_header_name)
)
@app.post("/api/focus")
async def focus(
request: Request,
file: UploadFile = File(...),
buffer_ratio: float = Form(0.15),
detector: str = Form("subject"),
_auth: None = Depends(require_auth),
):
from app.vision import process_image
try:
payload = await file.read()
result = process_image(payload, file.filename or "upload", buffer_ratio=buffer_ratio, detector=detector)
return {
"filename": result["filename"],
"detector": result["detector"],
"method": result["method"],
"buffer_ratio": result["buffer_ratio"],
"detected_bbox": result["detected_bbox"],
"square_bbox": result["square_bbox"],
"source_size": result["source_size"],
"crop_data_url": result["crop_data_url"],
"annotated_data_url": result["annotated_data_url"],
}
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/focus/image")
async def focus_image(
request: Request,
file: UploadFile = File(...),
buffer_ratio: float = Form(0.15),
detector: str = Form("subject"),
_auth: None = Depends(require_auth),
):
from app.vision import process_image
try:
payload = await file.read()
result = process_image(payload, file.filename or "upload", buffer_ratio=buffer_ratio, detector=detector)
return StreamingResponse(BytesIO(result["crop_bytes"]), media_type="image/jpeg")
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc