A CUDA-first local video captioning workbench for MP4 files and open vision-language models, with first-class Jetson Orin support.
The MVP has two workflows:
- A browser workspace: select a local MP4, press play, and watch frame captions appear as the local backend finishes each queued frame.
- A CLI batch captioner: sample frames from one or more MP4 files and write structured JSONL for evaluation or downstream automation.
Create a Python environment:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -U pip
python -m pip install -e '.[video]'Run the environment check:
python -m cuda_local_vlm_video_captioning.env_checkSmoke-test the JSONL path without loading a model:
python scripts/make_smoke_video.py outputs/smoke.mp4
python -m cuda_local_vlm_video_captioning.cli \
--video outputs/smoke.mp4 \
--output outputs/stub-captions.jsonl \
--backend stub \
--sample-count 4That command path only needs OpenCV. It verifies package installation, video decoding, frame sampling, and JSONL writing before any model weights are loaded.
transformers-cuda is the primary MVP backend. Install a CUDA-capable PyTorch
build for your machine first, then install the Transformers extra:
python -m pip install -e '.[transformers]'
python -m cuda_local_vlm_video_captioning.env_check \
--require-cuda \
--require-transformersOn Jetson, generic PyPI PyTorch wheels are often not the right choice. For the Orin development system used for this MVP, JetPack 6.2 / CUDA 12.6 worked with:
python -m pip install \
torch==2.8.0 \
torchvision==0.23.0 \
--index-url=https://pypi.jetson-ai-lab.io/jp6/cu126
python -m pip install -e '.[transformers]'Run the CUDA CLI path:
python -m cuda_local_vlm_video_captioning.cli \
--video outputs/smoke.mp4 \
--output outputs/captions.jsonl \
--backend transformers-cuda \
--model-id Qwen/Qwen2-VL-2B-Instruct \
--model-family auto \
--sample-count 8 \
--torch-dtype float16 \
--max-new-tokens 768First model use may download weights from Hugging Face unless they are already cached locally.
For a real validation image through the browser API, start the browser server and run:
python -m cuda_local_vlm_video_captioning.model_smoke \
--server-url http://127.0.0.1:8765 \
--browser-enabled-only \
--output outputs/browser-ready-smoke.jsonlBy default, model_smoke downloads a small public image. Pass
--image /path/to/image.jpg for offline or fully reproducible smoke tests.
Start the local server on the CUDA machine:
python -m cuda_local_vlm_video_captioning.web \
--host 127.0.0.1 \
--port 8765 \
--backend transformers-cuda \
--model-id Qwen/Qwen2-VL-2B-Instruct \
--model-family auto \
--torch-dtype float16 \
--max-new-tokens 512 \
--output outputs/browser-captions.jsonlFrom another computer, forward the port over SSH:
ssh -L 8765:127.0.0.1:8765 user@CUDA_HOSTOpen:
http://127.0.0.1:8765
Select an MP4 in the browser and press play. The browser captures decoded video frames in playback order and sends them to the local backend one at a time. Captions are appended to the transcript as they finish.
The direct server-side POST /api/caption path is disabled by default. Enable
it only for trusted local videos by starting the server with
--allowed-video-dir /path/to/videos; requests outside that directory are
rejected.
The Performance drawer intentionally has only runtime safety controls:
VLM Model: chooses the local Transformers model for new frame captions. The server exposes its default model plus any candidates explicitly marked as browser-verified. Switching models opens a loading overlay, clears the previous Transformers runtime cache, and loads the selected model before new captions are queued.Frame Stride:1attempts every decoded frame;Ncaptures every Nth decoded frame.Min Gap Sec: minimum playback-time spacing between queued captions. This prevents several decoded frames from producing visually duplicate timestamps.Max Captions: caps the number of queued captions for a playback run.Caption Detail: controls prompt verbosity for new frame captions.shortasks for 1-2 concise sentences,normalasks for 2-4 sentences, anddetailedasks for 4-6 sentences with more scene and behavior context.Caption on play: disables automatic frame capture without changing server settings.
On Jetson-class hardware, caption generation can lag behind playback. That is
expected for this MVP. For faster runs, increase frame stride, lower max
captions, reduce caption detail, reduce --max-new-tokens, or use a larger
CUDA GPU.
- Browser playback queues frame caption jobs; model output appears when each request completes. This is not token streaming or realtime video analytics.
max_visible_animals_hintandcount_confidenceare scene/context hints only. Use a detector/tracker if counts must be treated as source-of-truth data.- The repo is a standalone local workbench, not a private application or production integration.
Each CLI invocation writes one JSONL record per input video. Browser frame
captioning appends one JSONL record per captured frame when --output is set.
See examples/example-output.jsonl for a compact example. The core fields are:
{
"video_id": "example",
"clip_summary": "A short scene summary.",
"frame_observations": [],
"behavior_labels": [],
"interaction_hints": [],
"max_visible_animals_hint": null,
"count_confidence": "unknown",
"uncertainty": [],
"disagreements": [],
"provenance": {}
}No CUDA:
python -m cuda_local_vlm_video_captioning.env_check --require-cudaIf torch.cuda.is_available() is false, install a PyTorch build that matches
your CUDA or JetPack version before installing the Transformers extra.
Model download or cache problems:
python -m cuda_local_vlm_video_captioning.env_check --require-transformersCheck network access, Hugging Face cache location, disk space, and model id.
For private or gated checkpoints, set HF_TOKEN in the server environment.
Out of memory:
- Use
--torch-dtype float16. - Increase browser
Frame Stride. - Increase browser
Min Gap Sec. - Lower browser
Max Captions. - Use browser
Caption Detail: short. - Reduce
--max-new-tokens. - Restart the server after a failed large run.
Video cannot be opened:
- Verify the path exists for CLI usage.
- Try a standard H.264 MP4.
- Install OpenCV with
python -m pip install -e '.[video]'ifcv2is missing.
Malformed model JSON:
The parser repairs a few common near-JSON responses, then stores non-JSON model
text in clip_summary with an uncertainty note. Treat the structured fields as
model-generated hints, not validated facts.
Run tests:
python -m unittest discover -s testsRun a syntax check:
python -m compileall -q src testsProject layout:
src/cuda_local_vlm_video_captioning/cli.py: batch JSONL CLI.src/cuda_local_vlm_video_captioning/web.py: dependency-free browser server.src/cuda_local_vlm_video_captioning/backends/: backend adapter boundary.src/cuda_local_vlm_video_captioning/sampler.py: OpenCV frame sampling.src/cuda_local_vlm_video_captioning/schema.py: output normalization.
transformers-cuda: local PyTorch / Transformers baseline.stub: development backend for validating video sampling and JSONL output without loading a model.
transformers-cuda supports a --model-family switch so model weights and
model-specific inference APIs can vary independently:
auto: infer the family from--model-id.chat,qwen-vl,smolvlm: chat-template image/text models. These use the full structured JSON prompt and can receive multiple sampled frames at once.internvl: InternVL custom chat API. Current support captions one frame at a time and synthesizes the shared JSONL shape with behavior/count fields left untrusted.moondream2: Moondream query/caption API. Caption-only one-frame loop.florence2: Florence-2 task-prompt captioning. Caption-only one-frame loop.
The built-in candidate list is for benchmarking, not a guarantee that every model is browser-ready on every CUDA stack. The browser picker only exposes the server default plus candidates that have been verified locally. See docs/model-compatibility.md for the support matrix, commands used for validation, and current caveats.
Browser-verified in the main Transformers 5.7 runtime on the tested Orin stack:
Qwen/Qwen2-VL-2B-Instruct: current server default. Loads through the browser preload endpoint and returns structured single-frame captions.HuggingFaceTB/SmolVLM2-2.2B-Instruct: loads through the browser preload endpoint and returns coherent single-frame captions; natural-language output is normalized into the shared JSONL shape when the model does not emit JSON.
Browser-verified through the isolated Transformers 4.49 worker overlay:
vikhyatk/moondream2: fails with malformed repeated text in the main Transformers 5.7 runtime, but returns coherent BF16 captions through the worker overlay.OpenGVLab/InternVL2-4B: generation fails in the main Transformers 5.7 runtime because the checkpoint's remote language model does not exposegenerate(), but works through the worker overlay.microsoft/Florence-2-large: loading fails in the main Transformers 5.7 runtime with a remote config attribute error, but works through the worker overlay.
Install the shared worker overlay without reinstalling CUDA PyTorch:
python -m pip install \
--target /tmp/vlm-worker-overlays/transformers-4.49 \
--no-deps \
-r requirements/model-workers/transformers-4.49-overlay.txtEnable those worker-backed candidates in the browser picker:
VLM_TRANSFORMERS_WORKER_PYTHONPATH=/tmp/vlm-worker-overlays/transformers-4.49 \
python -m cuda_local_vlm_video_captioning.web \
--host 127.0.0.1 \
--port 8765 \
--backend transformers-cuda \
--model-id Qwen/Qwen2-VL-2B-Instruct \
--model-family auto \
--torch-dtype float16 \
--max-new-tokens 512 \
--output outputs/browser-captions.jsonlWorker-backed models run in a subprocess so their Transformers 4.x remote code does not downgrade or poison the main Qwen/SmolVLM runtime. First caption latency is high because each worker request loads the model in that subprocess.
List the built-in benchmark candidates:
python -m cuda_local_vlm_video_captioning.bench --list-candidatesRun one clip against one or more candidates:
python -m cuda_local_vlm_video_captioning.bench \
--video /path/to/video.mp4 \
--candidate qwen-vl=Qwen/Qwen2-VL-2B-Instruct \
--candidate smolvlm=HuggingFaceTB/SmolVLM2-2.2B-Instruct \
--candidate florence2=microsoft/Florence-2-large \
--sample-count 2 \
--max-new-tokens 384 \
--output outputs/model-bench.jsonlThe benchmark JSONL records include status, latency, peak CUDA memory when available, errors, and the generated caption record.
Gate browser readiness through the same preload and frame-caption endpoints the browser uses:
python -m cuda_local_vlm_video_captioning.model_smoke \
--browser-enabled-only \
--output outputs/browser-ready-smoke.jsonlSee docs/model-compatibility.md for the browser readiness gate, current model status, and isolated worker environment pins for models that need a different Transformers runtime.
Additional service adapters can be added later once they are tested against a real local CUDA VLM server.
MIT
