Async FastAPI + RQ backend for serving opensportslib inference from a separate project directory.
- Exposes HTTP endpoints for a frontend to submit inference jobs
- Queues jobs in Redis using RQ
- Runs a separate worker process that keeps the active model loaded in-process for reuse across same-model jobs
- Supports:
- VQA via
video_path + question - VQA via direct
upload_file - Classification via generated OSL JSON manifests
- Localization via generated OSL JSON manifests
- Session-aware API responses across all three tasks
- VQA via
- This project does not modify your local
opensportslibcheckout - Runtime should use any active Conda environment (the examples use
osl):
conda activate osl- OpenSportsLib should come from PyPI:
pip install opensportslib==0.3.0.dev20Install these in your active Conda environment:
pip install -r requirements.txtYou also need a Redis server running locally or reachable remotely.
On Ubuntu or DGX OS, if you have sudo, install and start Redis as a system service:
sudo apt update
sudo apt install -y redis-server
sudo systemctl enable --now redis-server
redis-cli pingThe expected response is PONG. This uses the default local URL, redis://127.0.0.1:6379/0. ./scripts/start_all.sh detects this running Redis service and reuses it.
If you do not have sudo, install the Redis server binary in Conda instead:
conda install -c conda-forge redisOr use the helper bootstrap script. It reuses an existing redis-server binary and installs Conda Redis only when no Redis server is available:
./scripts/setup_env.sh./scripts/setup_env.sh creates .env from .env.example when it is missing. If you skip that helper, copy .env.example to .env and fill in the model config and weights you want to serve.
This project now generates flat standalone configs directly under config/:
config/classification_video.standalone.yamlconfig/localization_video_dali.standalone.yamlconfig/vqa_qwen3_vl_native.standalone.yamlconfig/vqa_qwen2_5_vl_native.standalone.yamlconfig/vqa_xvars.standalone.yaml
These are fully merged standalone configs, so they do not depend on the
original OpenSportsLib configs/... folder structure.
Key environment entries are:
OSL_CLASSIFICATION_*for classificationOSL_LOCALIZATION_*for localizationOSL_VQA_QWEN3_*for the Qwen3 VL VQA modelOSL_VQA_QWEN25_*for the Qwen2.5 VL VQA modelOSL_VQA_XVARS_*for the X-VARS VQA model
Each model entry has:
..._MODEL_ENABLED..._MODEL_ID..._CONFIG_PATH..._WEIGHTS
The checked-in defaults are portable and repo-relative:
OSL_RUNTIME_DIR=./runtimeOSL_CLASSIFICATION_CONFIG_PATH=./config/classification_video.standalone.yamlOSL_LOCALIZATION_CONFIG_PATH=./config/localization_video_dali.standalone.yamlOSL_VQA_QWEN3_CONFIG_PATH=./config/vqa_qwen3_vl_native.standalone.yamlOSL_VQA_QWEN25_CONFIG_PATH=./config/vqa_qwen2_5_vl_native.standalone.yamlOSL_VQA_XVARS_CONFIG_PATH=./config/vqa_xvars.standalone.yaml
The default model_id values are the Hugging Face repo IDs themselves:
OpenSportsLab/OSL-cls-action-mvitv2OpenSportsLab/OSL-loc-snbas-2025-e2eOpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-loraOpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-loraOpenSportsLab/OSL-VQA-XFOUL-XVARS-lora
Pass model_id in the /predict request to select which one to use. The worker only keeps one active model loaded at a time, so it can switch models dynamically to reduce GPU RAM pressure.
With the current setup:
- Python env: any active Conda environment (for example,
conda activate osl) - OpenSportsLib package:
opensportslib==0.3.0.dev20 - Redis runs locally
- Configs are generated as flat standalone YAML files under
config/ model_idin API requests should be the Hugging Face repo IDweightsin.envare also the Hugging Face repo IDsstart_all.shcan predownload all listed Hugging Face model repos into cache- worker execution mode defaults to
simpleso the same loaded model can stay resident in RAM/GPU memory across same-model jobs - worker idle unload defaults to 10 minutes so unused GPU memory is eventually released
- Docker can optionally predownload all listed Hugging Face model repos into the mounted cache before worker startup
Set OSL_WORKER_EXECUTION_MODE in .env:
simpleKeeps inference in the main worker process. This is the recommended mode for GPU serving because the active model can remain loaded across requests and sessions.forkingUses the default RQ worker process model. This isolates jobs more strongly, but reloads model state on each job and is not recommended for your current inference-server use case.
Idle GPU release is controlled by OSL_WORKER_IDLE_UNLOAD_SECONDS:
- default:
600 - set to
0to disable idle unloading - when the worker stays idle longer than this threshold, it unloads the active model and clears CUDA cache
OpenSportsLib uses different dependency override profiles for:
- Qwen VQA
- X-VARS VQA
Use setup_env.sh with:
OSL_VQA_DEP_PROFILE=qwen ./scripts/setup_env.shor:
OSL_VQA_DEP_PROFILE=xvars ./scripts/setup_env.shOnly one profile should be active in the environment at a time.
./scripts/start_api.sh./scripts/start_worker.sh./scripts/start_all.sh supports both Redis management modes. It reuses a healthy Redis server already listening on the configured host and port; otherwise it starts the project-managed Redis binary.
If Redis is installed as a system service with sudo apt install redis-server, it is already running after:
sudo systemctl enable --now redis-serverYou can then run ./scripts/start_all.sh; it will reuse the system Redis service instead of starting a second instance.
If Redis is installed in your Conda environment, start the project-managed server with:
./scripts/start_redis.shThe project-managed Redis script accepts an explicit port. The API and worker must use the same port in OSL_REDIS_URL:
REDIS_PORT=6380 OSL_REDIS_URL=redis://127.0.0.1:6380/0 ./scripts/start_all.shUse this when you want the project-managed Redis server to use a different port. If a healthy Redis service already occupies 6379, start_all.sh reuses it automatically.
GET /healthPOST /predictGET /jobs/{job_id}GET /jobs/{job_id}/result
Every successful /predict response now includes:
job_idsession_id
VQA supports follow-up questions on the same session. Classification and localization can also reuse the same session_id, but their behavior is different:
- no new input: return the latest successful cached result from that session
- new input: create a new job under the same session
POST /predict accepts both:
application/jsonforvideo_pathormedia_urlmultipart/form-dataforupload_file
{
"task_type": "vqa",
"video_path": "/abs/path/to/video.mp4",
"question": "What card should be given?",
"model_id": "OpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-lora"
}conda activate osl
cd /path/to/opensportslib-server
./scripts/setup_env.sh./scripts/start_all.shBy default this also predownloads all configured Hugging Face repos into cache before starting Redis, API, and worker. To skip that step:
OSL_PREDOWNLOAD_ON_START=false ./scripts/start_all.shThis same command works with the system Redis service installed with sudo; the existing Redis process is reused.
curl http://127.0.0.1:8000/healthcurl -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-d '{
"task_type": "vqa",
"model_id": "OpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-lora",
"video_path": "/path/to/video.mp4",
"question": "What is happening in this clip?"
}'Example response:
{
"job_id": "8c76c3dd-7d88-46e4-bde4-c4a6cb11aee8",
"session_id": "50ca0608-0d90-4730-a4a0-0f9f74a09ecb",
"status": "queued",
"task_type": "vqa",
"model_id": "OpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-lora",
"created_at": "2026-08-13T10:00:00.000000Z"
}Use the same /predict endpoint with multipart form data and the file field name upload_file:
curl -X POST http://127.0.0.1:8000/predict \
-F "task_type=vqa" \
-F "model_id=OpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-lora" \
-F "question=What is happening in this clip?" \
-F "upload_file=@/path/to/video.mp4"Use the session_id returned by the first VQA request. Do not send a new file or video path on the follow-up.
curl -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-d '{
"task_type": "vqa",
"session_id": "50ca0608-0d90-4730-a4a0-0f9f74a09ecb",
"question": "Was this a foul?"
}'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-XVARS-lora",
"video_path": "/path/to/video.mp4",
"question": "What is happening in this clip?"
}'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": "/path/to/video.mp4"
}'Classification also returns a session_id.
If you send the same session_id again:
- with no new input, the API returns the latest successful cached result from that session
- with a new
video_pathorupload_file, the API creates a new job under the same session
curl -X POST http://127.0.0.1:8000/predict \
-H "Content-Type: application/json" \
-d '{
"task_type": "localization",
"model_id": "OpenSportsLab/OSL-loc-snbas-2025-e2e",
"video_path": "/absolute/path/to/video.mp4"
}'Localization also returns a session_id.
If you send the same session_id again:
- with no new input, the API returns the latest successful cached result from that session
- with a new
video_pathorupload_file, the API creates a new job under the same session
curl http://127.0.0.1:8000/jobs/<job_id>The job status response includes session_id so the frontend can keep the request associated with the session.
curl http://127.0.0.1:8000/jobs/<job_id>/resultIf a classification or localization request is repeated with the same session_id and no new input, /predict can return the cached result immediately. In that case the response includes:
{
"reused_result": true
}Results are also saved on disk at:
runtime/results/<job_id>.json./scripts/stop_all.shstop_all.sh also clears server-generated runtime state so storage does not accumulate unnecessarily:
- uploaded temp files
- job metadata
- job result files
- session state files
- Redis runtime files under
runtime/redis
While the server is running, uploaded media is kept for active sessions so VQA follow-up requests can reuse the same video. Expired sessions are cleaned automatically along with their associated runtime artifacts.
- Redis:
logs/redis.log - API:
logs/api.log - Worker:
logs/worker.log
- Every accepted request creates or belongs to a session.
- Session TTL defaults to 30 minutes of idle time and is controlled by
OSL_SESSION_TTL_SECONDS. - VQA can reuse a session for follow-up questions on the same video.
- Classification and localization reuse the same session for workflow continuity.
- Classification and localization with no new input return the latest cached result from that session immediately.
- Classification and localization with new input create a new job under the same session.
- VQA follow-up requests must not send a new
upload_file,video_path, ormedia_url. - VQA follow-up requests create a new
job_idunder the samesession_id.
This project also includes a Docker deployment path with Conda already installed in the image.
This path is intended for the same backend behavior as the non-Docker setup:
/predictsupports JSON andupload_file- successful requests return
job_idandsession_id - worker uses
simpleexecution mode unless overridden - worker can keep the active model loaded and also unload it after
OSL_WORKER_IDLE_UNLOAD_SECONDS
Files:
Dockerfiledocker-compose.yml
- starts from
continuumio/miniconda3 - creates a Conda env named
osl - installs Python 3.12
- installs
requirements.txt - generates the flat standalone configs under
/app/config - runs
opensportslib setupfor the selected VQA dependency profile when the container starts
This is intentional because the live container setup path matched the working host behavior better than running opensportslib setup during docker build.
For this server's request-driven inference flow, Docker only needs the runtime, logs, config, and Hugging Face cache mounts. It does not need a dataset directory mount for /predict requests that send video_path, media_url, or upload_file.
The Compose file mounts the Hugging Face cache like this:
${HOST_HF_CACHE:-./runtime/huggingface-cache}:/root/.cache/huggingfaceThe Docker launcher creates .env from .env.example automatically on its first run. Set or adjust these values in .env before building when needed:
OSL_DOCKER_VQA_DEP_PROFILE=qwenUseqwen,xvars, ornoneHOST_HF_CACHE=./runtime/huggingface-cacheHost Hugging Face cache path to reuse downloads
The worker service is configured with:
gpus: allSo Docker must have working NVIDIA container support on the host. Without that, the container can still build, but GPU inference will not work correctly.
From the project root:
./scripts/docker_compose.sh build
./scripts/docker_compose.sh up -dRunning ./scripts/docker_compose.sh without arguments also creates .env when needed and starts the stack with up -d.
This starts:
redisapiworker
At container startup, the api and worker services will first run opensportslib setup for the selected OSL_DOCKER_VQA_DEP_PROFILE, then launch the service process.
Optional Docker predownload is also supported. In v1, this is intended to run on the worker service only so the shared mounted Hugging Face cache is filled once before the worker starts.
Set these in .env if you want that behavior:
OSL_DOCKER_PREDOWNLOAD_ON_START=trueOSL_DOCKER_PREDOWNLOAD_WORKER_ONLY=true
With that setting:
workerpredownloads all repos listed byscripts/predownload_hf_assets.py- downloads go into the mounted
HOST_HF_CACHE apiskips the predownload step and still starts normally
If you want both api and worker to attempt predownload, set:
OSL_DOCKER_PREDOWNLOAD_WORKER_ONLY=falseThe recommended v1 setting is to keep OSL_DOCKER_PREDOWNLOAD_WORKER_ONLY=true.
If you want the X-VARS Docker image instead of Qwen, rebuild with:
OSL_DOCKER_VQA_DEP_PROFILE=xvars ./scripts/docker_compose.sh build --no-cache
./scripts/docker_compose.sh up -dChanging OSL_DOCKER_VQA_DEP_PROFILE requires restarting the containers so startup-time setup runs again.
Changing Docker predownload settings also requires restarting the containers:
./scripts/docker_compose.sh down
./scripts/docker_compose.sh up -d./scripts/docker_compose.sh ps
./scripts/docker_compose.sh logs -f api
./scripts/docker_compose.sh logs -f worker
./scripts/docker_compose.sh logs -f redisAfter ./scripts/docker_compose.sh up -d, check ./scripts/docker_compose.sh logs -f worker to confirm opensportslib setup completed before sending inference requests.
If Docker predownload is enabled, the worker logs should also show that Hugging Face assets were downloaded before opensportslib setup began.
curl http://127.0.0.1:8000/healthYou can then use the same /predict, /jobs/{job_id}, and /jobs/{job_id}/result commands documented earlier in this README.
./scripts/docker_stop_all.shThis stops the containers and clears runtime artifacts created by Docker. If you only want to stop containers without cleanup, use:
./scripts/docker_compose.sh downIf you have older nobody:nogroup runtime files from earlier Docker runs, docker_stop_all.sh is the preferred cleanup path because it performs the runtime wipe from inside Docker as root.
./scripts/docker_compose.sh build
./scripts/docker_compose.sh up -dUse the same Hugging Face repo IDs as model_id:
OpenSportsLab/OSL-cls-action-mvitv2OpenSportsLab/OSL-loc-snbas-2025-e2eOpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-loraOpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-lora