zamog commited on
Commit
d34a7e2
·
verified ·
1 Parent(s): e9de07e

Add FastAPI serving example

Browse files
Files changed (3) hide show
  1. serving/.env.example +22 -0
  2. serving/README.md +35 -0
  3. 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)