Skip to content

About

ScreenLens — Local video scene intelligence for Apple Silicon

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ScreenLens

Python 3.11+ DGX Spark Apple Silicon LangGraph Version Author License

Local video scene intelligence for NVIDIA DGX Spark and Apple Silicon. ScreenLens extracts meaningful frames from screen recordings, captions or transcribes them with a local multimodal model, embeds them with OpenCLIP, indexes them in ChromaDB, and reconstructs visible code, documents, PDFs, and GUI walkthroughs. Linux/ARM64 uses vLLM + CUDA by default; Apple Silicon uses oMLX + MPS. Ollama remains an optional fallback.

Demo

ScreenLens ingestion pipeline

Architecture

graph TD
    A[Video Input<br/>.mov / .mp4] --> B[Hybrid Keyframe Detection<br/>SSIM + pHash + HSV]
    B --> C{Local vision backend}
    C -->|DGX Spark| D[vLLM<br/>NVIDIA Qwen3.6 NVFP4]
    C -->|Apple Silicon| E[oMLX<br/>configured multimodal model]
    C -->|optional| F[Ollama vision]
    D --> G[Captions / Verbatim OCR]
    E --> G
    F --> G
    G --> H[OpenCLIP Embeddings<br/>CUDA / MPS / CPU]
    H --> I[ChromaDB Search]
    G --> J[LangGraph Reconstruction<br/>plan → workers → QA]
    J --> K[Reconstructed Artifacts<br/>data/*/output]
Loading

The vLLM and oMLX paths share the same small OpenAI-compatible client, including image data URLs and chat_template_kwargs.enable_thinking=false for Qwen-style models. Platform detection changes defaults, not the pipeline itself.

Component Overview

Component Technology Purpose
Frame extraction Hybrid SSIM + pHash + HSV detection Capture distinct screens while bounding long static intervals
DGX vision/text nvidia/Qwen3.6-27B-NVFP4 via vLLM Caption frames, perform OCR, summarize, and reconstruct through one dense local model
Apple vision/text Configured multimodal model via oMLX Native Apple Silicon OpenAI-compatible inference
Optional fallback Ollama Alternative captioning and summarization when explicitly selected
Visual embeddings OpenCLIP ViT-B-32 Semantic image/text vectors on CUDA, MPS, or CPU
Vector storage ChromaDB Persistent per-video semantic search
Reconstruction LangGraph Classify recordings and rebuild code/docs/demo artifacts with QA retries
Interfaces Typer + Rich, web command deck CLI and browser GUI

Platform Defaults

Host Inference Embeddings Concurrent image requests
Linux ARM64 / DGX Spark vLLM CUDA 2
Darwin ARM64 / Apple Silicon oMLX MPS 4
Other hosts oMLX unless overridden CPU 4

Caption output is capped at 32,768 tokens by default. The bundled dense Spark service exposes a 262,144-token total context, leaving ample prompt and image-token headroom while reconstruction can use the full served window.

Installation

NVIDIA DGX Spark

The checked helper creates an isolated Python 3.12 CUDA environment, validates the host and OpenCLIP, starts or reuses the exact vLLM model, and performs a real vision smoke test:

(umask 077; touch .env)
chmod 600 .env
${EDITOR:-nano} .env  # add HF_TOKEN=hf_... if ScreenLens must start vLLM

./setup_and_run_dgx.sh doctor
./setup_and_run_dgx.sh setup
./setup_and_run_dgx.sh llm-up
./setup_and_run_dgx.sh llm-wait
./setup_and_run_dgx.sh smoke
./setup_and_run_dgx.sh run

run launches the web command deck (serve --no-browser) with no arguments or passes a CLI command through:

./setup_and_run_dgx.sh run ingest input-videos/demo.mov
./setup_and_run_dgx.sh run transcribe input-videos/demo.mov

DigitalTwin uses the same model and loopback port. The helper reuses an already-ready exact-model service instead of starting a conflicting container. See the complete DGX Spark guide for CUDA wheel pins, cache sharing, memory sizing, service ownership, and troubleshooting.

Apple Silicon / Conda

Start oMLX on http://127.0.0.1:8000/v1, configure MLX_* or OMLX_* values in .env, then use the existing Conda launcher:

./setup_and_run_macos.sh
./setup_and_run_macos.sh ingest "video.mov"

The script creates a screenlens Conda environment with Python 3.11 and ffmpeg, installs the package in editable mode, preserves an existing .env, and defaults to the web command deck (serve). Set SCREENLENS_CONDA_ENV to choose another environment name. On Linux/ARM64 it exits with directions to the checked Spark helper so a generic pip install cannot replace CUDA 13 wheels with CPU wheels.

Manual development install

pip install -e ".[dev]"
pytest tests/ -v

Install ffmpeg separately and ensure the selected inference server is running. Ollama is not required for the normal vLLM or oMLX paths.

Usage

Provider Selection

Commands that contact the direct inference service accept provider-neutral flags:

python -m src.cli ingest video.mov \
  --backend vllm \
  --inference-url http://127.0.0.1:8000/v1 \
  --inference-model nvidia/Qwen3.6-27B-NVFP4 \
  --inference-api-key local \
  --device cuda \
  --batch-size 2

--vllm-url, --vllm-model, and --vllm-api-key are provider-specific aliases; the existing --omlx-* spellings remain compatible. SCREENLENS_BACKEND, SCREENLENS_DEVICE, and SCREENLENS_BATCH_SIZE override platform defaults. Provider credentials and models can also come from VLLM_*, MLX_*, or OMLX_* variables as appropriate.

Model Roles

ScreenLens drives two roles against one OpenAI-compatible endpoint:

Role Used by Requirement oMLX default
vision captioning, verbatim OCR must be vision-capable — a text-only model answers "no image provided" for every frame Qwen3.6-27B-bf16 (OCR_MODEL / MLX_MODEL)
text summaries, reconstruction plan/QA, transcript cleanup never sees an image; must stay coherent on markdown with indented lists, which is what captions are DeepSeek-V4-Flash-0731-MXFP4-MLX (LLM_MODEL)

On DGX Spark both resolve to the single served vLLM checkpoint. On Apple Silicon the defaults split the roles — Qwen3.6 sees, DeepSeek-V4-Flash reasons — and they resolve independently, so pointing LLM_MODEL at Qwen3.6-27B-bf16 to keep one resident checkpoint is fully supported.

The DeepSeek text default is guarded against the family's known failure mode on ScreenLens inputs. DeepSeek-V4-Flash-0731-MLX (the older 8-bit build) under oMLX emits its BOS token until the token budget runs out when a multi-sentence system prompt accompanies a markdown list item with a leading indent (\n - ), which every caption has. ScreenLens folds the system prompt into the user turn for deepseek-v4 ids — the verified fix — and detects any output that still degenerates, raising InferenceDegenerateError rather than saving it as a summary, so a failure is loud rather than silent. If you see that error, fall back to Qwen3.6-27B-bf16 for the text role.

Check what your endpoint serves and which side of the vision line each model falls on:

python -m src.cli models

The capability guard aborts a transcribe run whose OCR model is text-only, and the command deck's vision selector only offers models that can see.

Ingest and Search

# Uses vLLM/CUDA on DGX Spark or oMLX/MPS on Apple Silicon.
python -m src.cli ingest "video.mov"

# Optional Ollama captioning fallback.
python -m src.cli ingest "video.mov" --backend ollama --strategy fixed_fps --fps 1

# Search one or all timestamped collections and synthesize an answer locally.
python -m src.cli search "What application is shown?" --data-dir ./data --top-k 5

# Ingest and search in one command.
python -m src.cli run "video.mov" "Summarize the workflow"

Search summarization follows the configured platform backend; it does not require Ollama unless Ollama was explicitly selected. Each ingestion creates data/<stem>_<YYYYMMDD_HHMMSS>/ with independent frames, captions, and ChromaDB data.

Re-running a pipeline on the same video resumes the newest prior run instead of paying for the same work twice: ingestion reuses the extracted frames, any per-frame captions already written, and a populated vector store; transcription reuses cached OCR. Resume only engages when the run's stored video path/size and extraction config still match — pass --fresh to force a new run folder (supported by ingest, run, batch, and transcribe; the web deck resumes automatically).

Repeated work is avoided at other layers too: the CLIP embedder is a process-wide shared instance, so the web deck and multi-collection searches load OpenCLIP once instead of per query; search and full-video summaries are cached per run folder in summary_cache.json (keyed by model + prompt content); captioning keeps all pending frames in flight in a single pool with batch_size workers rather than stalling at each chunk boundary; and verbatim OCR sends frames to the server as in-memory JPEG q92 re-encodes — on-disk frames stay lossless PNG — shrinking multi-MB payloads roughly 5–10×.

Batch Ingestion and Full-Video Summary

python -m src.cli batch input-videos/
python -m src.cli summarize

Batch ingestion gives every video its own timestamped directory. Full-video summary reads all stored captions and chooses a single-pass or hierarchical strategy based on the selected backend's configured context window.

Reconstruct and Assemble Artifacts

python -m src.cli reconstruct
python -m src.cli assemble

Reconstruction classifies each recording, plans the artifact work, processes it deterministically, and runs up to three QA iterations before saving under data/<slug>/output/. Completed folders are skipped, Pass-1 segment notes are persisted to output/segment_notes.json so an interrupted run resumes cheaply, and the planning generation is cached across QA retries. Assembly can combine per-recording outputs into one project tree under OUTPUT/.

Verbatim Transcription

# Discover served models and their likely vision capability.
python -m src.cli models

# OCR frames, stitch scrolling overlap, and preserve raw text.
python -m src.cli transcribe input-videos/policies.mov

# Optional seam/indent cleanup; guarded against dropped content.
python -m src.cli transcribe input-videos/document.mov --cleanup

# Apple-only deterministic Vision cross-check for code.
python -m src.cli transcribe input-videos/code.mov --deterministic

The transcribe path copies visible text rather than describing it. A live image probe rejects blind/text-only deployments before a full run. Cleanup is off by default; when enabled, a per-chunk coverage guard keeps the raw stitched chunk whenever the model drops content. Outputs are transcript.raw.md, transcript.md, ocr/all_ocr.json, and metadata under the timestamped data directory.

Web command deck

python -m src.cli serve                 # http://127.0.0.1:8760, opens a browser
python -m src.cli serve --port 9000 --no-browser
python -m src.web                       # same thing without Typer

A local dashboard over every pipeline: run register, frame gallery served straight off disk, semantic search, pipeline control, and an artifact reader. It is stdlib http.server plus one static page — no framework, no build step, and no dependency beyond what ScreenLens already installs.

It binds loopback-only and refuses non-loopback clients on every /api/ route, because it starts jobs and reads frames off disk. One job runs at a time: the pipelines share a single model endpoint and one CLIP device.

There is no stop button. A running job cannot be cancelled from the page: interrupting it would mean aborting the in-flight HTTP request to the model server, which the stdlib client cannot do mid-read. Jobs run as daemon threads, so stopping the deck (Ctrl-C) ends them — the model server may keep generating the current request for a while afterwards. This matters more than it sounds: reconstruct over a large caption set can run for hours (see Performance Notes), so check the run size before starting one.

The header shows both model roles at once. If the configured vision model is not vision-capable the badge turns red before you waste a run on it, and the vision selector only offers models that can actually see.

Status

python -m src.cli info

Prints vector-store stats for the collections under ./data/. For anything interactive — picking models, starting pipelines, browsing frames and artifacts — use the web command deck above; it is the primary GUI.

Keyframe Detection

The hybrid change detector uses three complementary signals to decide when the screen has actually changed:

Signal What it detects Threshold
SSIM Pixel-level structural changes < 0.97
pHash Perceptual content changes via DCT hamming >= 8
HSV histogram Color-distribution shifts correlation <= 0.90

A keyframe is emitted when any signal triggers and the minimum 0.5-second interval has elapsed. A forced keyframe every four seconds catches slow scrolling. This typically captures a small fraction of source frames while preserving distinct screens.

Configuration

All settings live in src/config.py as Pydantic models. Key parameters:

Parameter Default Description
frame_extraction.strategy keyframe Smart change detection or fixed_fps
frame_extraction.max_interval_seconds 4.0 Maximum gap between keyframes
captioning.backend platform-dependent vllm, omlx, or optional ollama
captioning.vllm_base_url http://127.0.0.1:8000/v1 DGX Spark OpenAI-compatible API URL
captioning.vllm_model null Falls back to VLLM_MODEL, then the checked NVIDIA Qwen model
captioning.omlx_base_url http://127.0.0.1:8000/v1 Apple oMLX URL; root/dashboard URLs are normalized
captioning.omlx_model null Falls back to MLX_MODEL/OMLX_MODEL/LLM_MODEL, then default
captioning.batch_size 2 DGX / 4 Apple Concurrent direct-server caption requests
captioning.max_tokens 32768 Requested caption ceiling; vLLM uses the context remaining after image/prompt tokens
captioning.retry_attempts 1 Per-frame retries after a direct caption request fails
captioning.retry_max_tokens 2048 Bounded retry ceiling that prevents malformed generations from consuming another full caption budget
captioning.repetition_penalty 1.05 Discourage pathological long caption loops
captioning.no_repeat_ngram_size 12 Block repeated long caption sequences; 0 disables
captioning.disable_thinking true Spend the output budget on the visible answer
embedding.model_name ViT-B-32 OpenCLIP architecture
embedding.device CUDA DGX / MPS Apple Accelerator for OpenCLIP; CPU elsewhere
ocr.backend platform-dependent Direct vllm or omlx vision endpoint
ocr.concurrency 2 DGX / 4 Apple Concurrent verbatim OCR requests
reconstruction.backend platform-dependent Direct text inference endpoint
reconstruction.timeout_seconds 1800 Timeout for long-running reconstruction generations

Performance Notes

Reconstruction cost on Apple Silicon

reconstruct gives every extraction pass the whole model context as its output ceiling, so vLLM can compute the exact remaining completion space per prompt. On Spark that is close to free. On oMLX it is not: a measured 20-caption run (98k characters of captions) spent over three hours in Pass 1 alone, because each of its three segments generated 20–30k tokens at 6–12 tok/s and two of them exhausted the 32K ceiling and split into two passes each.

Before reaching for reconstruct on Apple Silicon, check whether summarize is what you actually want — it chunks to a bounded output and finished the same run in four minutes.

Direct backends receive one OpenAI-compatible image request per frame. Image encoding and prompt prefill usually dominate short captions, so the main levers are frame dimensions, model size, and request concurrency. The bundled Spark service admits two sequences and ScreenLens therefore defaults to two requests there. Apple defaults to four, but large oMLX models may benefit from a lower value. Concurrent results are isolated per frame: one failed request cannot overwrite a successful peer in the same chunk. A failed frame is retried once deterministically with a bounded 2K completion ceiling. A retry that fills that ceiling without terminating is rejected instead of storing a truncated loop; ScreenLens stores an error marker for that frame alone.

DGX Spark has one 128 GB unified-memory pool rather than separate system RAM and VRAM. The checked single-service recipe uses the dense NVFP4 27B model, a 0.45 vLLM allocator target, a 262K context, and concurrency two. Caption requests retain a 32K output ceiling, leaving the rest of the served window for prompt, chat-template, and image tokens. If a service is deliberately reduced to a matching 32K context, ScreenLens omits the literal output limit and lets vLLM allocate the exact context remaining after its input.

Reconstruction groups captions by serialized size rather than frame count, so an unusually long caption cannot overfill a later prompt. Its shared extraction pass requests dense notes under 1,400 tokens but gives the model the full context exposed by the server as completion headroom. Recursive synthesis filters notes to the file or artifact currently being rebuilt and uses that same full ceiling. These long text generations use the independent 1,800-second reconstruction.timeout_seconds budget instead of the shorter caption timeout. If a response still ends with finish_reason=length, ScreenLens discards the incomplete prefix and retries a smaller input group. The 262K service context is therefore used automatically when VLLM_MAX_MODEL_LEN matches it. Native two-token Qwen MTP accelerates decoding without changing answers; DFlash is not loaded because it would add a second draft model and does not increase context or output quality. See the Spark memory notes.

ffprobe remains optional: when it is absent, video metadata is read through OpenCV without a warning. OpenCLIP's checked checkpoint is public, so ScreenLens suppresses only the Hub's unauthenticated-download advisory; actual authorization, rate-limit, and download failures remain visible. A Hugging Face token is still required when the DGX helper must start the gated vLLM model service.

Project Structure

compose.dgx-spark.yaml # Bounded ARM64/CUDA vLLM service
docs/
  DGX_SPARK.md         # Setup, sizing, reuse, lifecycle, troubleshooting
setup_and_run_dgx.sh   # Checked Spark setup, validation, and launcher
setup_and_run_macos.sh # Apple Silicon Conda/oMLX launcher
src/
  cli.py               # Stable Typer command entry point
  config.py            # Shared platform-aware Pydantic settings
  session.py           # Config loading, model roles, and run discovery
  inference/
    client.py          # Provider-neutral Spark/oMLX client
    captioner.py       # Frame captioning
    ocr.py             # Verbatim vision OCR
  video/
    frame_extractor.py # Keyframe extraction
    frame_select.py    # Dense sampling for transcription
    stitch.py          # Scroll-overlap text stitching
  storage/
    embedder.py        # OpenCLIP embeddings
    vector_store.py    # ChromaDB storage and search
  workflows/
    pipeline.py        # Ingest/search/summarize graphs
    transcribe.py      # OCR and guarded cleanup
    reconstruct.py     # Artifact reconstruction and QA
    assemble.py        # Corpus assembly
  web/
    server.py          # Loopback HTTP API and static assets
    runner.py          # Background job coordination
    static/
      index.html       # Dashboard markup
      deck.css         # Nightshift-style presentation
      deck.js          # Dashboard interactions and API calls
      favicon.svg
tests/
  test_pipeline.py     # Core configuration, inference, embedding, and graph tests
  test_transcribe.py   # Stitching, OCR guards, and cleanup safety
  test_web.py          # Command deck assets, API contracts, and security
  test_cases.yaml      # Dual-platform/DGX end-to-end scenarios

Generated frames, captions, databases, videos, outputs, virtual environments, and model caches are ignored by Git.

How It Compares to NVIDIA VSS

Feature NVIDIA VSS ScreenLens
Frame extraction Custom + TensorRT Hybrid SSIM/pHash/HSV detection
Vision model NVIDIA VILA NVIDIA Qwen3.6 via vLLM or a configured oMLX model
Embeddings TensorRT visual encoder OpenCLIP ViT-B-32
Vector DB Milvus ChromaDB
LLM Llama 3.1 70B / NIM Same local vLLM/oMLX endpoint used by the pipeline
Hardware NVIDIA GPU DGX Spark or Apple Silicon
Deployment Docker + NIM Bounded vLLM container + Python, or native oMLX + Python
Cloud dependency None when self-hosted None; local model endpoints only

Roadmap

Duplicate Detection

  • Harden near-duplicate keyframe filtering (perceptual hash + SSIM fusion threshold tuning)
  • Cross-video deduplication for multi-file ingestion
  • Consider leveraging Karpathy's autoresearch to iterate on dedup thresholds against a fixed evaluation set

Video Profiles

Preconfigured extraction and captioning strategies tailored to content type:

Profile Description Audio Typical Source
code Silent screen recording of browsing/editing code No IDE walkthroughs, code reviews
demo Screencast with voice-over demonstrating software Yes Product demos, tutorials, onboarding
pdf Continuous scroll/browse of a PDF No Recorded read-throughs, slide decks
meeting Video call or presentation Yes Zoom/Teams recordings, webinars

Each profile will tune frame extraction, captioning prompts, chunking, and audio processing.

Audio Support

  • Integrate Whisper speech-to-text via ONNX Runtime and/or MLX
  • Support small, medium, and large model sizes
  • Add word-level timestamps aligned to the keyframe timeline
  • Fuse captions and transcripts for richer semantic search

Output Generators

  • Manual generator with extracted screenshots and navigation flow
  • PDF summary preserving headings, key points, and figures
  • Source-code reconstruction with files, signatures, and project structure
  • Meeting notes with action items, decisions, and speaker attribution

Command deck: Spark and oMLX

Run screenlens serve to open the Nightshift-style command deck. Choose Spark cluster · vLLM for http://sparkone.local:8000/v1, or oMLX · Apple Silicon for http://127.0.0.1:8000/v1. These presets match Nightshift; the endpoint field remains editable for other cluster addresses. Endpoint checks and model lists follow the selected backend and URL, and jobs use those same form values. Select a served vision model for captions/OCR and a served text model for reconstruction. The deck supports only these two inference backends; existing CLI compatibility is retained.

The Spark preset uses the cluster hostname so a changed LAN IP does not leave it pointing at an old address. Qwen3.8 models (including the served alias qwen3.8-flash-next) are recognized for captions/OCR. If an endpoint check fails, correct the URL or restore the server, then click Retry connection. An unreachable endpoint is reported separately from a reachable server with no recognized vision models; switching endpoints clears the old model choices.

Package boundaries

Shared configuration and session helpers stay at the package root. Inference handles model requests, video handles frames and stitching, storage handles embeddings and retrieval, and workflows coordinate those components. The CLI and web runner call workflows. Internal imports now use these explicit package paths; screenlens, python -m src.cli, and python -m src.web are unchanged. Design history lives in docs/REDESIGN.md. Local data, environment backups, agent state, and tool caches stay ignored and are not moved by this refactor.

About

ScreenLens — Local video scene intelligence for Apple Silicon

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages