196 lines
5.7 KiB
Python
196 lines
5.7 KiB
Python
"""FastAPI backend for the everything_function web UI.
|
|
|
|
This server doesn't do any AI work itself. It imports the same `ai_xxx`
|
|
functions used by the terminal demo scripts and exposes them as HTTP
|
|
endpoints so a browser can call them. The point: the web UI is just
|
|
another front-end for the *same Python functions* you can run from
|
|
`scripts/`. Nothing magic is happening in here.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
|
|
SCRIPTS_DIR = Path("/app/scripts")
|
|
SAMPLES_DIR = Path("/app/sample_images")
|
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
|
|
import action_list # noqa: E402
|
|
import algebra_roots # noqa: E402
|
|
import arithmetic # noqa: E402
|
|
import image_label # noqa: E402
|
|
import ocr # noqa: E402
|
|
import prime_factorization # noqa: E402
|
|
import recipe_from_food # noqa: E402
|
|
import sentiment # noqa: E402
|
|
import summarize # noqa: E402
|
|
import translate # noqa: E402
|
|
|
|
app = FastAPI(title="everything_function")
|
|
|
|
|
|
# ------ arithmetic ------------------------------------------------------------
|
|
|
|
class ArithRequest(BaseModel):
|
|
op: str
|
|
a: float
|
|
b: float
|
|
|
|
|
|
@app.post("/api/arithmetic")
|
|
def api_arithmetic(req: ArithRequest):
|
|
if req.op not in arithmetic.OPS:
|
|
raise HTTPException(400, f"unknown op: {req.op!r}")
|
|
ai_fn, py_fn = arithmetic.OPS[req.op]
|
|
ai_result = ai_fn(req.a, req.b).strip().rstrip(".")
|
|
py_result = py_fn(req.a, req.b)
|
|
return {"ai": ai_result, "python": str(py_result)}
|
|
|
|
|
|
# ------ algebra ---------------------------------------------------------------
|
|
|
|
class AlgebraRequest(BaseModel):
|
|
coefficients: list[float]
|
|
|
|
|
|
@app.post("/api/algebra")
|
|
def api_algebra(req: AlgebraRequest):
|
|
if len(req.coefficients) < 2:
|
|
raise HTTPException(400, "need at least 2 coefficients")
|
|
polynomial = algebra_roots.format_polynomial(req.coefficients)
|
|
ai_result = algebra_roots.ai_polynomial_roots(req.coefficients)
|
|
py_result = algebra_roots.py_polynomial_roots(req.coefficients)
|
|
return {"polynomial": polynomial, "ai": ai_result, "python": py_result}
|
|
|
|
|
|
# ------ prime factorization ---------------------------------------------------
|
|
|
|
class PrimeRequest(BaseModel):
|
|
n: int
|
|
|
|
|
|
@app.post("/api/prime_factorization")
|
|
def api_prime(req: PrimeRequest):
|
|
ai_result = prime_factorization.ai_prime_factorize(req.n).strip().rstrip(".")
|
|
py_result = prime_factorization.py_prime_factorize(req.n)
|
|
return {"ai": ai_result, "python": py_result}
|
|
|
|
|
|
# ------ text demos ------------------------------------------------------------
|
|
|
|
class SentimentRequest(BaseModel):
|
|
text: str
|
|
|
|
|
|
@app.post("/api/sentiment")
|
|
def api_sentiment(req: SentimentRequest):
|
|
return {"result": sentiment.ai_sentiment(req.text)}
|
|
|
|
|
|
class TranslateRequest(BaseModel):
|
|
text: str
|
|
target_language: str
|
|
|
|
|
|
@app.post("/api/translate")
|
|
def api_translate(req: TranslateRequest):
|
|
return {"result": translate.ai_translate(req.text, req.target_language)}
|
|
|
|
|
|
class SummarizeRequest(BaseModel):
|
|
text: str
|
|
max_words: int = 30
|
|
|
|
|
|
@app.post("/api/summarize")
|
|
def api_summarize(req: SummarizeRequest):
|
|
return {"result": summarize.ai_summarize(req.text, max_words=req.max_words)}
|
|
|
|
|
|
class ActionListRequest(BaseModel):
|
|
description: str
|
|
|
|
|
|
@app.post("/api/action_list")
|
|
def api_action_list(req: ActionListRequest):
|
|
return {"result": "- " + action_list.ai_action_list(req.description)}
|
|
|
|
|
|
# ------ image demos -----------------------------------------------------------
|
|
|
|
def _resolve_image(file: UploadFile | None, sample: str | None) -> Path:
|
|
"""Save the upload to /tmp, or return the named sample image's path."""
|
|
if file is not None and file.filename:
|
|
suffix = Path(file.filename).suffix.lower() or ".jpg"
|
|
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
|
|
try:
|
|
tmp.write(file.file.read())
|
|
return Path(tmp.name)
|
|
finally:
|
|
tmp.close()
|
|
if sample:
|
|
path = SAMPLES_DIR / sample
|
|
if not path.exists():
|
|
raise HTTPException(404, f"sample image not found: {sample!r}")
|
|
return path
|
|
raise HTTPException(400, "must provide either an uploaded file or a sample name")
|
|
|
|
|
|
@app.post("/api/image_label")
|
|
def api_image_label(
|
|
file: UploadFile | None = File(None),
|
|
sample: str | None = Form(None),
|
|
):
|
|
path = _resolve_image(file, sample)
|
|
return {"result": image_label.ai_image_label(path)}
|
|
|
|
|
|
@app.post("/api/recipe_from_food")
|
|
def api_recipe(
|
|
file: UploadFile | None = File(None),
|
|
sample: str | None = Form(None),
|
|
):
|
|
path = _resolve_image(file, sample)
|
|
return {"result": recipe_from_food.ai_recipe_from_food(path)}
|
|
|
|
|
|
@app.post("/api/ocr")
|
|
def api_ocr(
|
|
file: UploadFile | None = File(None),
|
|
sample: str | None = Form(None),
|
|
):
|
|
path = _resolve_image(file, sample)
|
|
return {"result": ocr.ai_ocr(path)}
|
|
|
|
|
|
# ------ sample images ---------------------------------------------------------
|
|
|
|
@app.get("/api/sample_images")
|
|
def list_samples():
|
|
return [
|
|
{"name": p.name, "url": f"/api/sample_images/{p.name}"}
|
|
for p in sorted(SAMPLES_DIR.glob("*.jpg"))
|
|
]
|
|
|
|
|
|
@app.get("/api/sample_images/{name}")
|
|
def get_sample(name: str):
|
|
path = SAMPLES_DIR / name
|
|
if path.suffix.lower() not in {".jpg", ".jpeg", ".png"} or not path.exists():
|
|
raise HTTPException(404)
|
|
# Pin to the samples directory so we can't escape via "../" tricks.
|
|
if path.resolve().parent != SAMPLES_DIR.resolve():
|
|
raise HTTPException(404)
|
|
return FileResponse(path)
|
|
|
|
|
|
# Static frontend served from /
|
|
app.mount("/", StaticFiles(directory="/app/static", html=True), name="static")
|