Skip to content

snigenigmatic/localens

Repository files navigation

Localens

CI Manual Benchmark Project status MCP

Airgapped multimodal local file search engine for Linux / WSL.

Indexes text, PDF, image, audio, and video files on your machine and lets you search any modality with a query of any modality — text, image, audio, or video. Fully local. No cloud. No telemetry.

Exposes the index over a local HTTP API, an MCP server (for Claude Desktop, Hermes, OpenClaw), and a CLI.

Project status and next work items are tracked in WORK_ITEMS.md. For deeper operational docs, see Architecture Notes, Agent And MCP Setup, and Operations Playbook.

Features

  • Any-to-any search: text → images, image → videos, audio → text, and every other combination across the 4×5 modality matrix
  • Fully airgapped: after one-time model download, runs 100% offline. Zero network calls at index or query time
  • Multimodal indexing: text, PDF, images, audio, video — all in one shared embedding space
  • Incremental: only changed files get re-embedded. Deleted files are swept automatically
  • MCP server: agents (Claude Desktop, Hermes, OpenClaw) can call search_files as a tool
  • Web UI: search box with drag-and-drop file upload, modality filters, thumbnail previews
  • CLI: scriptable commands for indexing, searching, and status

Performance Snapshot

Measured on WSL2 with jinaai/jina-embeddings-v5-omni-small, CUDA, 1024-dim embeddings, and exact generated benchmark corpora.

Run Files Index throughput Search p95 @ c1/c2/c4 Failures
Corpus 100 serial index-only 100 2.25 files/s n/a 0
Corpus 100 bounded-threaded index-only 100 2.36 files/s n/a 0
Corpus 100 full benchmark 100 2.61 files/s 161.9 / 289.8 / 678.5 ms 0
Corpus 200 full benchmark 200 2.76 files/s 179.9 / 317.4 / 711.9 ms 0
Corpus 500 bounded-threaded index-only 500 2.16 files/s n/a 0
Corpus 500 load on existing index 500 235.44 files/s incremental 184.1 / 342.7 / 892.2 ms 0
Corpus 1000 bounded-threaded index-only 1000 2.15 files/s n/a 0
Corpus 1000 load on existing index 1000 413.13 files/s incremental 229.7 / 410.6 / 898.0 ms 0

The 100-file serial-vs-threaded index-only comparison showed a 4.7% wall-time reduction and 5.0% throughput increase for bounded threaded preprocessing, with about 174 MiB higher max RSS.

The 500-file threaded run exposed a native PDFium crash under concurrent PDF preprocessing; Localens now serializes PDF rendering while keeping other modalities parallel. The rerun completed 500/500 files with zero failures.

Model

Uses jinaai/jina-embeddings-v5-omni-small (~1.74B params, 1024-dim embeddings, Matryoshka-truncatable).

License: CC BY-NC 4.0 (non-commercial). Personal and research use is fine. Commercial use requires a license from Jina.

trust_remote_code: This model uses custom Python code loaded via trust_remote_code=True. Pin the model revision for safety. See Security section.

Lighter alternative for low-VRAM/CPU: jinaai/jina-embeddings-v5-omni-nano (~0.95B). Set model.id in config.

Quick Start

Prerequisites

  • Python 3.11+
  • uv (recommended) or pip
  • ffmpeg (for audio/video codec support)
  • NVIDIA GPU with CUDA (optional; CPU works but is slower)

Install

git clone <this-repo> && cd localens

# Create environment and install
uv venv .venv
source .venv/bin/activate
uv pip install -e "."

# Direct executable path from this checkout
./bin/localens --help

One-time model download

# Run with internet access — downloads the model (~3.5GB)
LOCALENS_OFFLINE=false localens serve
# Or use Python directly:
# python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('jinaai/jina-embeddings-v5-omni-small', trust_remote_code=True)"

After download, all subsequent runs are offline by default.

Configure

Create a localens.toml in the project root or ~/.config/localens/config.toml:

[index]
directories = ["/home/you/documents", "/home/you/photos"]

[model]
device = "auto"   # "cuda", "cpu", or "auto"
truncate_dim = 1024

[server]
port = 8077

Run

# Start the server + web UI
localens serve

# Open http://127.0.0.1:8077 in your browser

# Or use the CLI
localens index /path/to/your/files
localens search "machine learning frameworks"
localens status

For direct PATH access without activating the virtual environment:

mkdir -p ~/.local/bin
ln -sf "$PWD/bin/localens" ~/.local/bin/localens
localens --help

The bin/localens launcher resolves the checkout path even when symlinked, uses .venv/bin/localens when available, and falls back to uv run localens.

CLI Reference

localens serve [--config PATH]        Start the API server + web UI
localens scan [PATHS...]              Count eligible files without indexing
localens index [PATHS...] [--full]    Index files (uses config dirs if no paths given)
  --min-files INT                     Require at least this many eligible files
  --max-files INT                     Index at most this many eligible files
  --confirm-large                     Allow an unbounded large indexing job
localens cancel-index JOB_ID          Request cancellation of an indexing job
localens search "QUERY" [OPTIONS]     Search with a text query
  -m, --modalities TEXT               Comma-separated: text,image,audio,video,pdf
  -k, --top-k INT                     Number of results (default: 20)
  -d, --dir PATH                      Restrict to directory
  --json                              Output as JSON
  --group                             Group results by file
localens search-file PATH [OPTIONS]   Search by file (image/audio/video)
localens status                       Show index stats and health
localens watch [--config PATH]        Start file watcher (foreground)
localens mcp [--config PATH]          Start MCP server on stdio

REST API

All endpoints bound to 127.0.0.1:8077 by default. If server.token is set, requires Authorization: Bearer <token> on all routes except /health.

Method Path Description
GET /health Health check (no auth required)
GET /stats Per-modality counts, index size, failures
POST /index/scan Count eligible files without indexing
POST /index Start indexing {paths?: [], full?: bool}
GET /index/{job_id} Poll indexing job status
POST /index/{job_id}/cancel Request cancellation of an indexing job
POST /index/sweep Remove records for deleted files
DELETE /index Clear index {confirm: true}
POST /search Search (see below)
POST /search/upload Search by uploaded file (multipart)
GET /file?path=... Stream original file (path-confined)
GET /thumb?path=... Stream thumbnail

Search request body

{
  "query": "text query",
  "query_file": null,
  "query_files": null,
  "modalities": ["text", "image", "audio", "video", "pdf"],
  "top_k": 20,
  "min_score": 0.0,
  "dir": null,
  "group_by_file": false
}

Exactly one of query, query_file, or query_files must be set.

Index scan and bounded indexing

Use /index/scan before large jobs:

{
  "paths": ["/home/you/documents"],
  "min_files": 100,
  "max_files": 500
}

The response includes eligible/selected file counts, size, modality counts, and a path preview.

POST /index accepts the same guard fields plus full and confirm_large:

{
  "paths": ["/home/you/documents"],
  "full": false,
  "min_files": 100,
  "max_files": 500,
  "confirm_large": false
}

min_files fails the job before embedding if not enough eligible files are found. max_files caps the selected files. Unbounded jobs over the built-in large-job threshold require confirm_large=true.

MCP Server

For use with Claude Desktop, Hermes, OpenClaw, and other MCP-compatible agents. See Agent And MCP Setup for copy-paste client configs, WSL/Windows notes, and safe agent-indexing guidance.

# Start on stdio (agents spawn this)
localens mcp

# Or add to your agent's MCP config:
# { "command": "localens", "args": ["mcp"] }

Tools exposed

  • search_files(query, modalities?, top_k?, dir?) — text search across all modalities
  • search_by_file(path, modalities?, top_k?) — search by a file on disk
  • get_index_stats() — compact document counts, index health, model id, dimensions, failures, and last index time
  • list_benchmark_reports(limit?) — compact summaries of recent benchmark reports without raw path lists or logs
  • scan_index(paths?, min_files?, max_files?) — count eligible files without indexing (only if mcp.allow_index = true)
  • reindex(paths?, full?, min_files?, max_files?, confirm_large?) — trigger bounded indexing (only if mcp.allow_index = true)
  • get_index_job(job_id) — poll indexing status (only if mcp.allow_index = true)
  • cancel_index(job_id) — request indexing cancellation (only if mcp.allow_index = true)

Configuration Reference

All settings in localens.toml. Env var overrides prefixed LOCALENS_.

Key Default Description
index.directories [] Paths to index
index.exclude_globs node_modules, .git, etc. Glob patterns to skip
index.max_file_mb 200 Skip files larger than this
index.modalities all five Which modalities to index
index.text_chunk_tokens 512 Token count per text chunk
index.text_chunk_overlap 64 Overlap tokens between chunks
index.preprocess_workers 1 Parallel file preprocessing workers; keep 1 for serial indexing
index.queue_size 32 Max in-flight files waiting in the bounded preprocessing pipeline
index.embed_batch_size 32 Prepared text items per embedding batch
index.upsert_batch_size 128 Records per vector-store upsert batch
model.id jinaai/jina-embeddings-v5-omni-small HuggingFace model ID
model.device auto cuda, cpu, or auto
model.truncate_dim 1024 Embedding dimension (Matryoshka)
model.batch_size_text 32 Text embedding batch size
store.backend lancedb Vector store backend
store.path ~/.local/share/localens/index Index data directory
server.host 127.0.0.1 API bind address
server.port 8077 API port
server.token "" Bearer token (required for non-loopback)
watch.enabled false Enable file watcher
watch.debounce_ms 1500 Watcher debounce
app.offline true Set HF_HUB_OFFLINE at startup
mcp.allow_index false Allow agents to trigger indexing
mcp.benchmark_dir benchmarks Directory for MCP benchmark report discovery

WSL2 Setup

GPU (CUDA)

WSL2 uses the Windows host NVIDIA driver. Inside WSL, install only the CUDA toolkit (not the driver):

# Install CUDA toolkit inside WSL
# Follow: https://developer.nvidia.com/cuda-downloads (select WSL-Ubuntu)
# Then install PyTorch with CUDA support:
uv pip install torch --index-url https://download.pytorch.org/whl/cu124

Verify: python -c "import torch; print(torch.cuda.is_available())"

Filesystem performance

Files under /mnt/c (Windows drives) go through the 9p bridge and are slow. Recommend indexing files in the Linux filesystem (e.g., /home/you/). Localens prints a warning when indexing /mnt/ paths.

Web UI access

WSL2 forwards localhost, so http://127.0.0.1:8077 is reachable from Windows browsers directly.

ffmpeg

sudo apt install ffmpeg

Plain Linux Setup

# System deps
sudo apt install ffmpeg

# For GPU: install NVIDIA driver + CUDA toolkit
# Then install CUDA PyTorch:
uv pip install torch --index-url https://download.pytorch.org/whl/cu124

# CPU-only is fine too — model.device will auto-detect

Security

  • Loopback only: server binds to 127.0.0.1 by default. Refuses to start on non-loopback without server.token.
  • Token auth: when server.token is set, all routes except /health require Authorization: Bearer <token>.
  • Path confinement: /file route only serves files inside indexed directories. Rejects .. traversal and symlink escapes.
  • trust_remote_code: the model runs custom Python from its HuggingFace repo. Pin the revision to prevent code changes. Consider vendoring the model code locally.
  • No telemetry: zero outbound network calls. Enforced by offline=true default.
  • mcp.allow_index: defaults false so agents cannot trigger indexing/deletion without explicit opt-in.

Architecture

Architecture Option A (all-Python): single FastAPI process handles ingestion, embedding, vector storage, REST API, and serves the web UI. The MCP server runs as a separate stdio process. The CLI talks to the REST API.

files → File Walker → Preprocessors → Embedding Service → Vector Store (LanceDB)
                                                              ↑
query (any modality) → Search Service → ANN search ──────────┘
                            ↓
              REST API / MCP Server / CLI / Web UI

Supported File Types

Modality Extensions
Text .txt .md .markdown .rst .csv .json .log .html .xml + source code
Image .jpg .jpeg .png .gif .webp .bmp .tif .tiff .avif .heic .svg
Audio .wav .mp3 .flac .ogg .m4a .opus
Video .mp4 .avi .mov .mkv .webm .flv .wmv
PDF .pdf

License

MIT (this codebase). The embedding model (jina-embeddings-v5-omni) is CC BY-NC 4.0.

About

Airgapped multimodal local file search engine for Linux / WSL.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages