Skip to content

Repository files navigation

OpenSportsLib Server

Async FastAPI + RQ backend for serving opensportslib inference from a separate project directory.

What this project does

  • 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

Important assumptions

  • This project does not modify your local opensportslib checkout
  • 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.dev20

Installations to run

Install these in your active Conda environment:

pip install -r requirements.txt

You 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 ping

The 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 redis

Or 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

Configure models

./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.yaml
  • config/localization_video_dali.standalone.yaml
  • config/vqa_qwen3_vl_native.standalone.yaml
  • config/vqa_qwen2_5_vl_native.standalone.yaml
  • config/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 classification
  • OSL_LOCALIZATION_* for localization
  • OSL_VQA_QWEN3_* for the Qwen3 VL VQA model
  • OSL_VQA_QWEN25_* for the Qwen2.5 VL VQA model
  • OSL_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=./runtime
  • OSL_CLASSIFICATION_CONFIG_PATH=./config/classification_video.standalone.yaml
  • OSL_LOCALIZATION_CONFIG_PATH=./config/localization_video_dali.standalone.yaml
  • OSL_VQA_QWEN3_CONFIG_PATH=./config/vqa_qwen3_vl_native.standalone.yaml
  • OSL_VQA_QWEN25_CONFIG_PATH=./config/vqa_qwen2_5_vl_native.standalone.yaml
  • OSL_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-mvitv2
  • OpenSportsLab/OSL-loc-snbas-2025-e2e
  • OpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-lora
  • OpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-lora
  • OpenSportsLab/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.

Current setup

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_id in API requests should be the Hugging Face repo ID
  • weights in .env are also the Hugging Face repo IDs
  • start_all.sh can predownload all listed Hugging Face model repos into cache
  • worker execution mode defaults to simple so 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

Worker execution mode

Set OSL_WORKER_EXECUTION_MODE in .env:

  • simple Keeps 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.
  • forking Uses 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 0 to disable idle unloading
  • when the worker stays idle longer than this threshold, it unloads the active model and clears CUDA cache

VQA dependency profiles

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.sh

or:

OSL_VQA_DEP_PROFILE=xvars ./scripts/setup_env.sh

Only one profile should be active in the environment at a time.

Start the API

./scripts/start_api.sh

Start the worker

./scripts/start_worker.sh

Start Redis

./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-server

You 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.sh

The 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.sh

Use 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.

API endpoints

  • GET /health
  • POST /predict
  • GET /jobs/{job_id}
  • GET /jobs/{job_id}/result

Every successful /predict response now includes:

  • job_id
  • session_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/json for video_path or media_url
  • multipart/form-data for upload_file

Example request

{
  "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"
}

End-to-end usage

1. Setup

conda activate osl
cd /path/to/opensportslib-server
./scripts/setup_env.sh

2. Start services

./scripts/start_all.sh

By 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.sh

This same command works with the system Redis service installed with sudo; the existing Redis process is reused.

3. Check health

curl http://127.0.0.1:8000/health

4. Run VQA

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-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"
}

4a. Run VQA with direct file upload

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"

4c. Run a VQA follow-up on the same session

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?"
  }'

4b. Run VQA with X-VARS

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?"
  }'

5. Run classification

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_path or upload_file, the API creates a new job under the same session

6. Run localization

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_path or upload_file, the API creates a new job under the same session

7. Check job status

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.

8. Get result

curl http://127.0.0.1:8000/jobs/<job_id>/result

If 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

9. Stop services

./scripts/stop_all.sh

stop_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.

Logs

  • Redis: logs/redis.log
  • API: logs/api.log
  • Worker: logs/worker.log

Session behavior

  • 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, or media_url.
  • VQA follow-up requests create a new job_id under the same session_id.

Docker

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:

  • /predict supports JSON and upload_file
  • successful requests return job_id and session_id
  • worker uses simple execution mode unless overridden
  • worker can keep the active model loaded and also unload it after OSL_WORKER_IDLE_UNLOAD_SECONDS

Files:

  • Dockerfile
  • docker-compose.yml

What the image does

  • 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 setup for 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.

Important runtime note

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/huggingface

Docker environment values

The 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=qwen Use qwen, xvars, or none
  • HOST_HF_CACHE=./runtime/huggingface-cache Host Hugging Face cache path to reuse downloads

GPU requirement

The worker service is configured with:

gpus: all

So Docker must have working NVIDIA container support on the host. Without that, the container can still build, but GPU inference will not work correctly.

Build and run with Docker Compose

From the project root:

./scripts/docker_compose.sh build
./scripts/docker_compose.sh up -d

Running ./scripts/docker_compose.sh without arguments also creates .env when needed and starts the stack with up -d.

This starts:

  • redis
  • api
  • worker

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=true
  • OSL_DOCKER_PREDOWNLOAD_WORKER_ONLY=true

With that setting:

  • worker predownloads all repos listed by scripts/predownload_hf_assets.py
  • downloads go into the mounted HOST_HF_CACHE
  • api skips the predownload step and still starts normally

If you want both api and worker to attempt predownload, set:

OSL_DOCKER_PREDOWNLOAD_WORKER_ONLY=false

The 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 -d

Changing 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

Check service status

./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 redis

After ./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.

Test the API

curl http://127.0.0.1:8000/health

You can then use the same /predict, /jobs/{job_id}, and /jobs/{job_id}/result commands documented earlier in this README.

Stop Docker services

./scripts/docker_stop_all.sh

This stops the containers and clears runtime artifacts created by Docker. If you only want to stop containers without cleanup, use:

./scripts/docker_compose.sh down

If 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.

Rebuild after code changes

./scripts/docker_compose.sh build
./scripts/docker_compose.sh up -d

Model IDs in Docker requests

Use the same Hugging Face repo IDs as model_id:

  • OpenSportsLab/OSL-cls-action-mvitv2
  • OpenSportsLab/OSL-loc-snbas-2025-e2e
  • OpenSportsLab/OSL-VQA-XFOUL-qwen3-8B-VL-lora
  • OpenSportsLab/OSL-VQA-XFOUL-qwen2.5-7B-VL-lora

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages