Add FastAPI serving example
Browse files- serving/.env.example +22 -0
- serving/README.md +35 -0
- serving/app.py +67 -0
serving/.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy to .env — signsdk auto-loads it.
|
| 2 |
+
|
| 3 |
+
# Device: cuda (GPU) or cpu. Needs the signsdk[gpu] install for cuda.
|
| 4 |
+
SIGNSDK_DEVICE=cuda
|
| 5 |
+
|
| 6 |
+
# Models kept resident (LRU; evicting frees the GPU). Each SpaMo is ~11.5 GB on GPU,
|
| 7 |
+
# so on a 24 GB card keep this at 1.
|
| 8 |
+
SIGNSDK_MAX_RESIDENT=1
|
| 9 |
+
|
| 10 |
+
# Fall back to CPU if fewer than this many MiB are free on the GPU (avoids OOM).
|
| 11 |
+
# SIGNSDK_SPAMO_GPU_MIB=22000
|
| 12 |
+
# SIGNSDK_UNISIGN_GPU_MIB=5000
|
| 13 |
+
|
| 14 |
+
# Cap input frames for SpaMo (native max_frame_length=512). Lower if a long clip OOMs.
|
| 15 |
+
# SIGNSDK_MAX_FRAMES=512
|
| 16 |
+
|
| 17 |
+
# VideoMAE batch (lower = less GPU memory). Default 8.
|
| 18 |
+
# SIGNSDK_MAE_BATCH=8
|
| 19 |
+
|
| 20 |
+
# Where models are cached (default ~/.cache/signsdk) / which HF repo to pull from.
|
| 21 |
+
# SIGNSDK_HOME=/data/signsdk
|
| 22 |
+
# SIGNSDK_HF_REPO=croco-corp/pjm-models
|
serving/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Serving the PJM models (FastAPI)
|
| 2 |
+
|
| 3 |
+
A minimal FastAPI server over [`signsdk`](https://pypi.org/project/signsdk) — upload a
|
| 4 |
+
sign-language video, get English text. Models are pulled from this HF repo on first use.
|
| 5 |
+
|
| 6 |
+
## Run
|
| 7 |
+
|
| 8 |
+
```bash
|
| 9 |
+
uv venv
|
| 10 |
+
uv pip install "signsdk[gpu]" # GPU; or "signsdk[cpu]" / plain "signsdk"
|
| 11 |
+
cp .env.example .env # optional: tune device + memory
|
| 12 |
+
uv run uvicorn app:app --host 0.0.0.0 --port 8000
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
**GPU note:** `rtmlib` pins CPU `onnxruntime`, so add this to your `pyproject.toml` to keep
|
| 16 |
+
the GPU wheel:
|
| 17 |
+
```toml
|
| 18 |
+
[tool.uv]
|
| 19 |
+
override-dependencies = ["onnxruntime ; sys_platform == 'never'"]
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Use
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
# list models
|
| 26 |
+
curl localhost:8000/models
|
| 27 |
+
|
| 28 |
+
# translate (default model = unisign-pjm-how2sign-ms)
|
| 29 |
+
curl -F video=@clip.mp4 -F model=spamo-pjm-phoenix-ms localhost:8000/translate
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
## Memory (`.env`)
|
| 33 |
+
Each SpaMo model is ~11.5 GB on GPU (~21 GB peak). On a 24 GB card set
|
| 34 |
+
`SIGNSDK_MAX_RESIDENT=1` — `signsdk.load()` then frees the resident model before loading a
|
| 35 |
+
new one. If the GPU is too full, it falls back to CPU instead of OOMing. See `.env.example`.
|
serving/app.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""FastAPI serving example for the PJM sign-language → English models (signsdk).
|
| 2 |
+
|
| 3 |
+
Quick start:
|
| 4 |
+
uv venv && uv pip install "signsdk[gpu]" # or signsdk[cpu] / plain signsdk
|
| 5 |
+
cp .env.example .env # tune device / memory
|
| 6 |
+
uv run uvicorn app:app --host 0.0.0.0 --port 8000
|
| 7 |
+
|
| 8 |
+
Endpoints:
|
| 9 |
+
GET / health + default model
|
| 10 |
+
GET /models list the 16 models (filter ?backend=unisign|spamo)
|
| 11 |
+
POST /translate upload a video, get the decoded text
|
| 12 |
+
form fields: model, num_beams, max_new_tokens
|
| 13 |
+
|
| 14 |
+
Memory: models are loaded via signsdk.load() — an LRU cache that frees the GPU when it
|
| 15 |
+
evicts. Each SpaMo model is ~11.5 GB / ~21 GB peak, so on a 24 GB card keep
|
| 16 |
+
SIGNSDK_MAX_RESIDENT=1 (see .env.example). If a model won't fit, signsdk falls back to CPU.
|
| 17 |
+
"""
|
| 18 |
+
import shutil
|
| 19 |
+
import tempfile
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Optional
|
| 22 |
+
|
| 23 |
+
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
| 24 |
+
|
| 25 |
+
import signsdk
|
| 26 |
+
from signsdk import GenerationConfig, SignTranslatorError
|
| 27 |
+
|
| 28 |
+
app = FastAPI(title="PJM sign-language translation", version="1.0")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@app.get("/")
|
| 32 |
+
def health():
|
| 33 |
+
return {"status": "ok", "default_model": signsdk.DEFAULT_MODEL}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@app.get("/models")
|
| 37 |
+
def models(backend: Optional[str] = None):
|
| 38 |
+
"""List available models. Filter with ?backend=unisign or ?backend=spamo."""
|
| 39 |
+
return {"default": signsdk.DEFAULT_MODEL, "models": signsdk.describe_models(backend)}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@app.post("/translate")
|
| 43 |
+
async def translate(
|
| 44 |
+
video: UploadFile = File(..., description="sign-language video clip (mp4)"),
|
| 45 |
+
model: str = Form(signsdk.DEFAULT_MODEL),
|
| 46 |
+
num_beams: int = Form(4),
|
| 47 |
+
max_new_tokens: int = Form(64),
|
| 48 |
+
):
|
| 49 |
+
"""Translate one uploaded sign-language video to text."""
|
| 50 |
+
suffix = Path(video.filename or "clip.mp4").suffix or ".mp4"
|
| 51 |
+
tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
|
| 52 |
+
try:
|
| 53 |
+
shutil.copyfileobj(video.file, tmp)
|
| 54 |
+
tmp.close()
|
| 55 |
+
gen = GenerationConfig(num_beams=num_beams, max_new_tokens=max_new_tokens)
|
| 56 |
+
try:
|
| 57 |
+
result = signsdk.load(model).predict(tmp.name, generation=gen)
|
| 58 |
+
except SignTranslatorError as e:
|
| 59 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 60 |
+
return {
|
| 61 |
+
"text": result.text,
|
| 62 |
+
"model": result.model,
|
| 63 |
+
"lang": result.lang,
|
| 64 |
+
"timings": result.timings,
|
| 65 |
+
}
|
| 66 |
+
finally:
|
| 67 |
+
Path(tmp.name).unlink(missing_ok=True)
|