OpenSportsLib inference server¶
The server is a FastAPI API backed by Redis and an RQ worker. It serves classification, localization, and VQA models through HTTP and OpenSportsLib.
Start the server¶
From the repository root, install the separate server project in Python 3.12+:
uv venv --python 3.12 .venv
source .venv/bin/activate
uv pip install -e ./server
cd server
./scripts/serverctl setup
./scripts/serverctl start
serverctl start starts or reuses Redis, the API, and the worker. Docker Compose
is available with ./scripts/serverctl docker start. A host video_path must
be visible inside the worker container; mount it read-only when using Docker.
See server/README.md for Docker, GPU, dependency,
and shutdown details.
Check readiness:
Important settings include OSL_REDIS_URL, OSL_RUNTIME_DIR,
OSL_MODEL_ROOT, optional OSL_API_KEY, optional HF_TOKEN, OSL_JOB_TIMEOUT_SECONDS,
OSL_MODEL_OPERATION_TIMEOUT_SECONDS, OSL_SESSION_TTL_SECONDS,
OSL_WORKER_EXECUTION_MODE, and OSL_WORKER_IDLE_UNLOAD_SECONDS.
OSL_JOB_STALE_GRACE_SECONDS adds a grace period before abandoned RQ jobs are
reconciled (default: 60 seconds).
Stale jobs and cleanup¶
The worker reconciles session metadata with RQ before cleaning expired sessions.
Jobs that are missing from RQ, terminal in RQ, or older than
OSL_JOB_TIMEOUT_SECONDS + OSL_JOB_STALE_GRACE_SECONDS are marked failed and
their sessions become cleanup-eligible. Confirmed queued/started jobs are not
deleted automatically.
Inspect without changing state:
curl -X POST http://127.0.0.1:8000/admin/runtime/reconcile \
-H "Authorization: Bearer $OSL_API_KEY" \
-H 'Content-Type: application/json' -d '{"dry_run":true}'
Apply recovery and cleanup:
curl -X POST http://127.0.0.1:8000/admin/runtime/reconcile \
-H "Authorization: Bearer $OSL_API_KEY" \
-H 'Content-Type: application/json' -d '{"dry_run":false}'
Do not manually delete Redis keys or runtime files while jobs may be running; use the dry-run response and worker/API logs first.
./scripts/serverctl clean removes transient request state while preserving
registrations and model caches. ./scripts/serverctl reset is a confirmed
factory reset that deletes Redis registrations, runtime data, downloaded and
local models, and logs; pass --yes only for unattended automation.
The same operation is available through the Python client or helper script:
from opensportslib import RemoteModelRegistry
registry = RemoteModelRegistry(
"http://127.0.0.1:8000",
api_key="<optional-api-key>",
)
print(registry.reconcile_runtime(dry_run=True))
print(registry.reconcile_runtime(dry_run=False))
The script is dry-run by default; --apply is required to modify state:
server/scripts/serverctl reconcile --server http://127.0.0.1:8000
server/scripts/serverctl reconcile --server http://127.0.0.1:8000 --apply
Docker credentials¶
server/.env.example is only a template. Docker Compose loads the actual
server/.env file:
Set a machine Hugging Face token and, when local/admin operations are needed, an API key:
Do not put a real token in .env.example, documentation, or Git. The token
shared during troubleshooting should be treated as exposed and rotated before
production use. Recreate the containers after changing it:
Verify that the API container received a token without printing its value:
docker compose exec api sh -c \
'test -n "$HF_TOKEN" && echo "HF token is set" || echo "anonymous HF access"'
Use the exact same private value in curl or Python:
from opensportslib import RemoteModelRegistry
registry = RemoteModelRegistry(
"http://10.64.74.111:8000",
api_key="<optional-api-key>",
)
If API-key authentication returns 401, check the OSL_API_KEY value and Bearer header.
Model registry¶
HF model registration/removal verifies repository access using a request token
or worker environment token. Local models, defaults, and runtime reconciliation
require Authorization: Bearer $OSL_API_KEY; leaving it empty disables those
operations. Public responses redact local filesystem paths and all credentials.
Register models¶
Hugging Face models use their repository ID as model_id:
curl -X POST http://127.0.0.1:8000/models \
-H 'Content-Type: application/json' \
-d '{"task_type":"classification","source":{"type":"huggingface","model_id":"OpenSportsLab/OSL-cls-action-mvitv2"}}'
For a private repository, add "hf_token":"hf_..." inside source. The
request token overrides HF_TOKEN, HUGGINGFACE_HUB_TOKEN, and
HUGGINGFACE_TOKEN from the worker environment and is deleted after the
operation. Public repositories may use anonymous access.
Server-local models must be below OSL_MODEL_ROOT:
curl -X POST http://127.0.0.1:8000/models \
-H "Authorization: Bearer $OSL_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"task_type":"localization","model_id":"football-model:v2","source":{"type":"local","weights_path":"/srv/models/football/model.pth","config_path":"/srv/models/football/config.yaml"}}'
A model directory discovers config.yaml; a weights file requires
config_path. Omitting a local ID generates local:<name>-<hash>. IDs may
contain letters, numbers, ., _, -, and :. Repeating the same
registration is idempotent; a different source for an existing ID returns 409.
Hugging Face repositories may be checkpoint-backed or configuration-only. The
server inspects the repository and config.yaml automatically: supported
checkpoint files use normal weight loading, while supported config-driven
runners load with weights=None. A config-only repository that declares no
supported weightless runner fails registration with an actionable operation
error. Runner detection uses canonical MODEL.metadata.runner after migrating
legacy configs; the historical TRAIN.runner field remains supported.
Checkpoints ending in .pth.tar are recognized alongside .pt, .pth, and
other supported checkpoint suffixes. This behavior is task-agnostic and does
not depend on a model ID.
Registration is asynchronous and returns an operation_id:
curl \
http://127.0.0.1:8000/model-operations/<operation_id>
curl http://127.0.0.1:8000/models
curl 'http://127.0.0.1:8000/models/status?model_id=football-model:v2'
Model states are registering, ready, failed, and unregistering.
Operations are queued, running, succeeded, or failed; inference only
accepts ready models.
Set a task default and unregister a model:
curl -X PUT http://127.0.0.1:8000/models/defaults/classification \
-H "Authorization: Bearer $OSL_API_KEY" \
-H 'Content-Type: application/json' -d '{"model_id":"OpenSportsLab/OSL-cls-action-mvitv2"}'
curl -X DELETE http://127.0.0.1:8000/models/football-model%3Av2 \
-H "Authorization: Bearer $OSL_API_KEY"
For a Hugging Face model, omit the API-key header and optionally send
X-HF-Token: hf_...; deletion succeeds only after repository access is verified.
Poll the returned unregister operation before reusing the ID.
The Python administration client exposes the same lifecycle:
from opensportslib import RemoteModelRegistry
r = RemoteModelRegistry("http://127.0.0.1:8000", api_key="secret")
op = r.register_model(task_type="classification",
huggingface_model_id="OpenSportsLab/OSL-cls-action-mvitv2")
r.wait_for_operation(op["operation_id"])
print(r.list_models())
print(r.get_model("OpenSportsLab/OSL-cls-action-mvitv2"))
r.set_default("classification", "OpenSportsLab/OSL-cls-action-mvitv2")
remove = r.unregister_model("OpenSportsLab/OSL-cls-action-mvitv2")
r.wait_for_operation(remove["operation_id"])
RemoteRegistryError preserves HTTP status and structured detail. Connection
and polling timeouts raise ConnectionError and TimeoutError.
Inference API¶
The Python task wrappers can be remote-only. When remote is configured, the
client does not need local config or weights; the registered server model
owns its configuration and checkpoint. remote_model_id is the server registry
ID and is independent of local weights.
from opensportslib.apis import VQAModel
vqa = VQAModel(
remote="http://10.64.74.111:8000",
remote_model_id="OpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-lora",
)
The wrappers retain the session_id returned by the first real request as
last_remote_session_id. VQA questions reuse it automatically; classification
and localization calls without new input reuse the latest cached result. Use
clear_remote_session() to reset it or pass an explicit session_id=.
All requests use POST /predict and return a queued job_id and session_id.
Poll and retrieve results with:
If model_id is omitted, the ready task default is used. Discover supported
request-specific overrides first:
curl 'http://127.0.0.1:8000/config-capabilities?task_type=vqa&model_id=OpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-lora'
Only advertised settings in task_options.config_overrides are accepted.
Single video¶
Use a server-readable path or configured URL:
curl -X POST http://127.0.0.1:8000/predict \
-H 'Content-Type: application/json' \
-d '{"task_type":"classification","model_id":"OpenSportsLab/OSL-cls-action-mvitv2","video_path":"/data/clip.mp4"}'
VQA also requires question; media_url can replace video_path where URL
retrieval is configured. For an uploaded file:
curl -X POST http://127.0.0.1:8000/predict \
-F task_type=vqa -F model_id=OpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-lora \
-F question='What happened?' -F upload_file=@clip.mp4
The Python wrappers upload local media when remote is set:
from opensportslib.apis import ClassificationModel
m = ClassificationModel(config="classification.yaml",
remote="http://127.0.0.1:8000",
remote_model_id="OpenSportsLab/OSL-cls-action-mvitv2")
predictions = m.infer(video_path="clip.mp4")
For direct remote VQA, provide both the video and question. The wrapper stores the returned session ID and automatically uses it for the follow-up:
from opensportslib.apis import VQAModel
vqa = VQAModel(remote="http://127.0.0.1:8000",
remote_model_id="OpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-lora")
answer = vqa.infer(video_path="clip.mp4", question="Was this a foul?")
print(vqa.last_remote_session_id)
follow_up = vqa.infer(question="What card should be given?")
To restore a session explicitly, pass session_id="<session_id>" to the
follow-up. A new video_path starts a new session automatically.
Entire test set¶
Send an OSL JSON manifest and a ZIP containing every referenced media file:
curl -X POST http://127.0.0.1:8000/predict \
-F task_type=classification \
-F model_id=OpenSportsLab/OSL-cls-action-mvitv2 \
-F test_set_file=@test.json -F media_archive=@media.zip
Manifest/archive uploads cannot be combined with a single-video input or an existing session.
One job per sample¶
predictions = m.infer(test_set="test.json", remote_mode="per_sample")
failures = m.last_remote_failures
Each sample and its referenced media are submitted independently. Successful
predictions are returned after individual failures, which are recorded in
last_remote_failures. full_test_set uses one upload/job; per_sample
provides isolation and partial progress.
With raw HTTP, repeat the multipart request for each sample and its matching media archive (the shell loop is application-specific):
curl -X POST http://127.0.0.1:8000/predict \
-F task_type=classification -F model_id=OpenSportsLab/OSL-cls-action-mvitv2 \
-F test_set_file=@sample-001.json -F media_archive=@sample-001-media.zip
Sessions and VQA follow-ups¶
curl -X POST http://127.0.0.1:8000/predict -H 'Content-Type: application/json' \
-d '{"task_type":"vqa","model_id":"OpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-lora","video_path":"clip.mp4","question":"Was this a foul?"}'
curl -X POST http://127.0.0.1:8000/predict -H 'Content-Type: application/json' \
-d '{"task_type":"vqa","session_id":"<session_id>","question":"What card should be given?"}'
Classification and localization may reuse a session with no new input to return
the latest successful cached result. The OpenSportsLib wrappers start a new
session automatically when a new video is supplied; direct HTTP callers may
explicitly reuse a session if desired. VQA follow-ups must not include a file, path, or URL. Sessions expire
after OSL_SESSION_TTL_SECONDS and then return 410.
Common errors are 401 invalid admin token, 404 unknown model/job/session, 409 model not ready or result unavailable, 422 invalid input/override, and 415 unsupported content type.