Skip to content

Latest commit

 

History

History
897 lines (698 loc) · 28.5 KB

File metadata and controls

897 lines (698 loc) · 28.5 KB

@localmode/core

Local-first AI utilities for the browser. Zero dependencies. Privacy-first.

npm license

Docs UI Components Blocks & Apps

Installation

# Preferred: pnpm
pnpm install @localmode/core

# Alternative: npm
npm install @localmode/core

Related Packages

Package Description
@localmode/transformers HuggingFace Transformers.js provider
@localmode/webllm WebLLM provider for local LLMs
@localmode/wllama GGUF model provider via llama.cpp WASM
@localmode/litert Google LiteRT-LM provider (.litertlm, WebGPU/WASM)
@localmode/react React hooks for all core functions
@localmode/chrome-ai Chrome Built-in AI provider
@localmode/langchain LangChain.js adapters
@localmode/devtools In-app DevTools widget
@localmode/mediapipe Google MediaPipe Tasks — hand/pose/face landmarks, gesture recognition, vision/audio/text
@localmode/pdfjs PDF text extraction
@localmode/dexie Dexie.js storage adapter
@localmode/idb Minimal idb storage adapter
@localmode/localforage Cross-browser storage adapter

Overview

@localmode/core is a zero-dependency package containing all functions, interfaces, types, and utilities for building local-first AI applications. Provider packages (like @localmode/transformers and @localmode/webllm) implement these interfaces with specific ML frameworks.

Features

Vector Database

  • HNSW index for fast approximate nearest neighbor search
  • IndexedDB persistence with memory fallback
  • Cross-tab synchronization via Web Locks and BroadcastChannel
  • Metadata filtering with extensible operators
  • Scalar quantization (SQ8) for 4x storage reduction with >95% recall
  • Product quantization (PQ) for 8-32x storage reduction (85-92% recall)
  • Storage compression (compression: { type: 'sq8' }) for 4x IndexedDB disk reduction without any search recall impact
  • Typed metadata with generic createVectorDB<TMetadata>() for compile-time filter safety
  • Optional Zod schema validation on add()/addMany()
  • TypedFilterQuery<TMetadata> for autocomplete on filter keys and operators

Pipelines — Docs

  • createPipeline() - Composable multi-step workflow builder
  • Type-safe step chaining with progress tracking
  • Pre-built step factories: embedStep, chunkStep, searchStep, rerankStep, storeStep, classifyStep, summarizeStep, generateStep
  • AbortSignal propagation between steps
  • PipelineError with step context (name, index, cause)

Inference Queue — Docs

  • createInferenceQueue() - Priority-based task scheduling
  • Configurable concurrency and priority levels (interactive > background > prefetch)
  • Stats events for monitoring (pending, active, completed, avgLatencyMs)
  • AbortSignal support for queued tasks
  • clear() and destroy() lifecycle methods

Embeddings — Docs

  • embed() - Generate embeddings for single values
  • embedMany() - Batch embedding with progress tracking
  • semanticSearch() - Embed a query and search a VectorDB; results[].text round-trips chunk text stored by ingest()
  • Middleware support for caching, logging, validation

Embedding Drift Detection — Docs

  • ModelFingerprint type tracks which model produced a collection's vectors
  • Automatic drift detection on VectorDB.initialize() with modelDriftDetected event
  • checkModelCompatibility() - Read-only compatibility check
  • reindexCollection() - Batch re-embedding with resumability, progress, cancellation
  • Inference queue integration at 'background' priority
  • Cross-tab locking via Web Locks API

Similarity Threshold Calibration — Docs

  • calibrateThreshold() - Empirical threshold from corpus embedding distribution
  • MODEL_THRESHOLD_PRESETS - Known-good defaults for popular embedding models
  • getDefaultThreshold() - Instant preset lookup
  • Distribution statistics (mean, median, stdDev, min, max) for observability
  • Configurable percentile, distance function, maxSamples, AbortSignal

Multimodal Embeddings

  • embedImage() - Embed images into the same vector space as text
  • embedManyImages() - Batch image embedding
  • streamEmbedManyImages() - Streaming batch image embedding with progress
  • MultimodalEmbeddingModel interface for CLIP/SigLIP providers
  • Cross-modal similarity search (text-to-image, image-to-text)

Reranking — Docs

  • rerank() - Document reranking for improved RAG accuracy

RAG Utilities — Docs

  • Text chunking (recursive, markdown, code-aware, semantic)
  • chunk() - Split documents into optimal chunks
  • semanticChunk() - Embedding-aware chunking at topic boundaries
  • ingest() - Chunk + embed + store in one call — positional ingest(db, documents, options?) or object ingest({ db, documents, model }) with model-driven embedding via embedMany(), AbortSignal support
  • TEXT_METADATA_FIELD - The shared metadata key ('_text') under which ingestion stores chunk text and semanticSearch() reads it back
  • createBM25() - BM25 keyword search
  • hybridFuse(), reciprocalRankFusion() - Hybrid search combining vector and keyword results
  • Document loaders (Text, JSON, CSV, HTML)

Knowledge Base Engine — Docs

  • createKnowledgeBaseEngine() - provider-agnostic KnowledgeBaseEngine (kind: 'core') wiring chunk (off/recursive/semantic) → embed → typed-metadata VectorDB, vector search, and grounded ask with <think>…</think> reasoning stripped
  • Models are injected (an EmbeddingModel + a lazy getLanguageModel()) — no provider dependency, no new core dependency
  • Frozen KnowledgeBaseEngine contract (RawDocument, KBSearchResult, IngestOptions, SearchOptions, AskOptions, AskResult, EngineStats, ChunkingMode, DocumentSource) implemented identically by the LangChain-adapter engine in @localmode/langchain
  • Re-ingest-safe store (removeDocument, clear, stats), PDF page attribution, AbortSignal honored between phases

WebGPU-Accelerated Vector Search — Docs

  • createGPUDistanceComputer() - GPU-accelerated batch distance computation via WGSL shaders
  • GPUDistanceManager - WebGPU device lifecycle, buffer pooling, pipeline caching
  • Cosine, euclidean, and dot product distance on GPU
  • enableGPU option on createVectorDB() for transparent GPU acceleration
  • Graceful CPU fallback when WebGPU is unavailable
  • Configurable batch threshold (default 64) — GPU for large batches, CPU for small

Agent Framework — Docs

  • createAgent() - Create reusable agents with tools, instructions, and optional memory
  • runAgent() - One-shot agent execution with ReAct loop
  • createToolRegistry() - Type-safe tool registration with Zod schemas
  • defineTool() - Type-inferring tool definition helper — parameters and execute(params) are checked against each other, and typed tools fit ToolDefinition[] without casts
  • createAgentMemory() - VectorDB-backed conversation memory with semantic retrieval
  • Max-step guards, loop detection, duration limits
  • Human-in-the-loop tool approval — flag a tool with requiresApproval: true and provide an onToolApproval callback (config-level or per-run): the loop pauses before executing the flagged tool, { approved: true } resumes it unchanged, { approved: false, reason? } skips execution and feeds a denial observation back so the model adapts; AgentStep.approval records the decision. A pending decision races the run's abortSignal (aborting rejects immediately without executing) and its wait time does not count toward maxDurationMs; a gated tool with no callback fails fast with AgentError before any model call
  • Works with any LanguageModel provider (WebLLM, wllama, transformers, litert, chrome-ai)

Vector Import/Export — Docs

  • importFrom() - Import vectors from Pinecone, ChromaDB, CSV, or JSONL
  • parseExternalFormat() - Parse and preview external formats without importing
  • exportToCSV(), exportToJSONL() - Export vector collections
  • convertFormat() - Standalone format-to-format conversion
  • Auto-detect format from content, re-embedding for text-only imports
  • Batch processing with progress callbacks and AbortSignal

Text Generation — Docs

  • streamText() - Streaming text generation with async iteration
  • generateText() - Complete text generation with LLMs
  • generateObject() - Generate typed, validated JSON objects with schema
  • streamObject() - Stream partial JSON objects with final validation
  • jsonSchema() - Convert Zod schemas for structured output
  • Vision/Audio/MultimodalContentPart union type (TextPart | ImagePart | AudioPart) for sending images and audio to multimodal models
  • normalizeContent(), getTextContent() - Multimodal content utilities
  • supportsVision flag on LanguageModel interface for feature detection
  • AbortSignal support for cancellation
  • Works with WebLLM (Phi 3.5 Vision), Transformers (Qwen3.5), wllama, and litert (Gemma 4 E2B/E4B + Qwen3 0.6B, text-only) for local LLM inference

Classification & NLP — Docs

  • classify(), classifyMany() - Text classification
  • classifyZeroShot() - Zero-shot classification with custom labels
  • extractEntities(), extractEntitiesMany() - Named Entity Recognition

Audio — Docs

  • transcribe() - Speech-to-text with Whisper / Moonshine models
  • synthesizeSpeech() - Text-to-speech synthesis (one-shot)
  • streamSynthesizeSpeech() - Streaming text-to-speech: clause-by-clause Float32Array audio for low first-audio latency
  • playStreamedSpeech() - Gap-free Web Audio playback for the streaming iterable, with pause / resume / stop / abort
  • splitIntoClauses() - Pure clause splitter (sentence + comma/semicolon boundaries, abbreviation- and decimal-aware)
  • Kokoro TTS via @localmode/transformers: 29 voices, speed control, phonemizer-backed synthesis
  • classifyAudio() - Audio classification
  • classifyAudioZeroShot() - Zero-shot audio classification
  • Word and segment-level timestamps, multi-language support

Live Transcribe — Docs

  • createLiveTranscriber() - Streaming microphone-driven STT with VAD
  • Push-to-talk and open-mic modes with auto-segmentation
  • Built-in EnergyVADProvider (zero-dependency, AudioWorklet-based) and SileroVADProvider adapter
  • Barge-in detection during external TTS playback
  • createTurnTaker() orchestrator for full voice loops (user → planner → agent → user)

Vision — Docs

  • classifyImage() - Image classification
  • classifyImageZeroShot() - Zero-shot image classification
  • captionImage() - Image captioning with BLIP/Florence models
  • segmentImage() - Image segmentation / background removal
  • detectObjects() - Object detection with bounding boxes
  • extractImageFeatures() - Image feature extraction for similarity
  • imageToImage() - Image super resolution / image-to-image transformation
  • estimateDepth() - Monocular depth estimation
  • detectHands() - 21-point hand landmark detection (MediaPipe)
  • detectPose() - 33-point body pose landmark detection (MediaPipe)
  • detectFace() - Face detection with bounding boxes and keypoints (MediaPipe)
  • detectFaceLandmarks() - 478-point face mesh detection (MediaPipe)
  • recognizeGesture() - Hand gesture recognition (MediaPipe)

Text Generation (Complete)

  • generateText() - Complete text generation with LLMs
  • wrapLanguageModel() - Language model middleware for caching, logging, guardrails
  • composeLanguageModelMiddleware() - Compose multiple middleware

Semantic Cache

  • createSemanticCache() - Cache LLM responses using embedding similarity
  • semanticCacheMiddleware() - Transparent caching middleware for generateText/streamText

Translation — Docs

  • translate() - Text translation between languages
  • detectLanguage() - Language detection (110 languages, ISO 639-1 codes)

Summarization — Docs

  • summarize() - Text summarization with configurable length

Fill-Mask — Docs

  • fillMask() - Masked token prediction (BERT-style)

Question Answering — Docs

  • answerQuestion() - Extractive question answering from context

OCR — Docs

  • extractText() - Optical character recognition from images

Document QA — Docs

  • askDocument() - Question answering on document images
  • askTable() - Question answering on tabular data

Storage — Docs

  • IndexedDBStorage - Persistent browser storage
  • MemoryStorage - In-memory fallback
  • Automatic quota management and cleanup

Tamper-evident Audit Log — Docs

  • createAuditLog() - Append-only, hash-chained, signed entries
  • HMAC-SHA-256 default; Ed25519 opt-in via injected CryptoKey
  • verifyChain() - Cancellable walker reports hash_mismatch / prev_hash_mismatch / signature_mismatch
  • Optional AES-GCM payload encryption — chain still verifies without the encryption key
  • exportAuditLog() - JSON Lines output for customer-controlled S3 / Azure Blob sync
  • deriveAuditKey() / generateEphemeralAuditKey() - PBKDF2 + ephemeral helpers
  • Zero-dep, no network calls — Web Crypto only

Evaluation SDK — Docs

  • accuracy(), precision(), recall(), f1Score() - Classification metrics
  • bleuScore(), rougeScore() - Text generation metrics
  • mrr(), ndcg() - Retrieval metrics
  • evalCosineDistance() - Vector quality metric
  • confusionMatrix() - Structured confusion matrix with helper methods
  • evaluateModel() - Orchestrator: run model against dataset, apply metric, get report
  • Custom metrics via MetricFunction type

Capabilities Detection — Docs

  • isWebGPUSupported() - Check WebGPU availability
  • detectCapabilities() - Full device capability report
  • Automatic fallback recommendations
  • computeOptimalBatchSize() - Adaptive batch sizing from device hardware (CPU cores, memory, GPU)
  • recommendModels() - Ranked model recommendations for any task based on device capabilities
  • registerModel() / getModelRegistry() - Curated model catalog with runtime extensibility

Quick Start

Semantic Search with PDF Documents

import { createVectorDB, embed, embedMany, chunk, rerank } from '@localmode/core';
import { transformers } from '@localmode/transformers';

// Create embedding model
const embeddingModel = transformers.embedding('Xenova/bge-small-en-v1.5');

// Create vector database
const db = await createVectorDB({
  name: 'documents',
  dimensions: 384,
});

// Chunk and embed document
const chunks = chunk(documentText, {
  strategy: 'recursive',
  size: 512,
  overlap: 50,
});

const { embeddings } = await embedMany({
  model: embeddingModel,
  values: chunks.map((c) => c.text),
});

// Store in database
await db.addMany(
  chunks.map((c, i) => ({
    id: `chunk-${i}`,
    vector: embeddings[i],
    metadata: { text: c.text },
  }))
);

// Search
const { embedding: queryVector } = await embed({
  model: embeddingModel,
  value: 'What is machine learning?',
});

const results = await db.search(queryVector, { k: 10 });

// Optional: Rerank for better accuracy
const rerankerModel = transformers.reranker('Xenova/ms-marco-MiniLM-L-6-v2');
const reranked = await rerank({
  model: rerankerModel,
  query: 'What is machine learning?',
  documents: results.map((r) => r.metadata.text),
  topK: 5,
});

Vector Quantization (4x Storage Reduction)

import { createVectorDB } from '@localmode/core';

// Enable scalar quantization — same API, 4x less storage
const db = await createVectorDB({
  name: 'quantized-docs',
  dimensions: 384,
  quantization: { type: 'scalar' },
});

// Usage is identical — quantization is transparent
await db.add({ id: 'doc1', vector: embedding, metadata: { text: 'Hello' } });
const results = await db.search(queryVector, { k: 10 });

// Recalibrate if vector distribution changes significantly
await db.recalibrate({ onProgress: (done, total) => console.log(`${done}/${total}`) });

LLM Chat with Streaming

import { streamText } from '@localmode/core';
import { webllm } from '@localmode/webllm';

const model = webllm.languageModel('Llama-3.2-1B-Instruct-q4f16_1-MLC');

const result = await streamText({
  model,
  prompt: 'Explain quantum computing in simple terms',
  maxTokens: 500,
});

for await (const chunk of result.stream) {
  process.stdout.write(chunk.text);
}

Core Exports

Vector Database

import {
  createVectorDB,
  createVectorDBWithWorker,
  HNSWIndex,
  cosineSimilarity,
  euclideanDistance,
  dotProduct,
} from '@localmode/core';

Embeddings

import {
  embed,
  embedMany,
  streamEmbedMany,
  semanticSearch,
  wrapEmbeddingModel,
} from '@localmode/core';

Multimodal Embeddings

import {
  embedImage,
  embedManyImages,
  streamEmbedManyImages,
} from '@localmode/core';

Reranking

import { rerank } from '@localmode/core';

RAG Utilities

import {
  // Chunking
  chunk,
  recursiveChunk,
  markdownChunk,
  codeChunk,
  // BM25
  createBM25,
  // Hybrid Search
  hybridFuse,
  reciprocalRankFusion,
  // Ingestion
  ingest,
  TEXT_METADATA_FIELD,
} from '@localmode/core';

Knowledge Base Engine

import { createKnowledgeBaseEngine } from '@localmode/core';
import type {
  KnowledgeBaseEngine,
  RawDocument,
  KBSearchResult,
  AskResult,
  EngineStats,
} from '@localmode/core';

const engine = createKnowledgeBaseEngine({
  embeddingModel, // an EmbeddingModel you construct (e.g. transformers.embedding(...))
  getLanguageModel: () => languageModel, // lazy — loaded on first ask()
});
await engine.ingest(docs, { chunking: 'recursive', chunkSize: 500 });
const hits = await engine.search('privacy and encryption', { topK: 10 });
const { answer, sources } = await engine.ask('What is encrypted?');

Text Generation

import { streamText, generateText } from '@localmode/core';

Storage

import {
  IndexedDBStorage,
  MemoryStorage,
  createStorage,
  getStorageQuota,
  requestPersistence,
  cleanup,
} from '@localmode/core';

Capabilities

import {
  detectCapabilities,
  isWebGPUSupported,
  isIndexedDBSupported,
  isAudioWorkletSupported,
  isMediaCaptureSupported,
  isLiveTranscribeSupported,
  checkModelSupport,
  getRecommendedFallbacks,
  recommendModels,
  registerModel,
  getModelRegistry,
  DEFAULT_MODEL_REGISTRY,
} from '@localmode/core';

Classification

import { classify, classifyMany, classifyZeroShot } from '@localmode/core';

NER

import { extractEntities, extractEntitiesMany } from '@localmode/core';

Audio

import {
  transcribe,
  synthesizeSpeech,
  streamSynthesizeSpeech,
  playStreamedSpeech,
  splitIntoClauses,
  DEFAULT_ABBREVIATIONS,
  classifyAudio,
  classifyAudioZeroShot,
} from '@localmode/core';

Live Transcription & VAD

import {
  createLiveTranscriber,
  createTurnTaker,
  EnergyVADProvider,
  SileroVADProvider,
  registerEnergyVADWorklet,
  createScriptProcessorVADNode,
  isLiveTranscribeSupported,
  isAudioWorkletSupported,
  isMediaCaptureSupported,
  MediaNotSupportedError,
} from '@localmode/core';

Vision

import {
  classifyImage,
  classifyImageZeroShot,
  captionImage,
  segmentImage,
  detectObjects,
  extractImageFeatures,
  imageToImage,
  estimateDepth,
  detectHands,
  detectPose,
  detectFace,
  detectFaceLandmarks,
  recognizeGesture,
  HAND_CONNECTIONS,
  POSE_CONNECTIONS,
  FACE_CONNECTIONS,
  GESTURE_CATEGORIES,
} from '@localmode/core';

Translation

import {
  translate,
  detectLanguage,
  SUPPORTED_LANGUAGES,
  getLanguageName,
} from '@localmode/core';

Summarization

import { summarize } from '@localmode/core';

Fill-Mask

import { fillMask } from '@localmode/core';

Question Answering

import { answerQuestion } from '@localmode/core';

OCR

Supports an optional prompt parameter for generative OCR models (e.g., GLM-OCR, LightOnOCR-2) to control recognition mode — table structure, formula/LaTeX, or plain text extraction.

import { extractText } from '@localmode/core';

Document QA

import { askDocument, askTable } from '@localmode/core';

Middleware

import {
  wrapEmbeddingModel,
  wrapVectorDB,
  cachingMiddleware,
  loggingMiddleware,
  retryMiddleware,
  rateLimitMiddleware,
  validationMiddleware,
  piiRedactionMiddleware,
  encryptionMiddleware,
} from '@localmode/core';

Security

import { encrypt, decrypt, deriveKey, isCryptoSupported, redactPII } from '@localmode/core';

Audit Log

import {
  createAuditLog,
  verifyChain,
  exportAuditLog,
  deriveAuditKey,
  generateEphemeralAuditKey,
  AuditLogError,
} from '@localmode/core';

Differential Privacy

import {
  // Embedding middleware — adds calibrated noise to embeddings
  dpEmbeddingMiddleware,
  // Classification middleware — randomized response for labels
  dpClassificationMiddleware,
  randomizedResponse,
  // Privacy budget tracking
  createPrivacyBudget,
  // Noise mechanisms
  gaussianNoise,
  laplacianNoise,
  // Sensitivity calibration
  calibrateSensitivity,
} from '@localmode/core';

Cross-Tab Sync

import { createBroadcaster, createLockManager, isWebLocksSupported } from '@localmode/core';

Network

import {
  getNetworkStatus,
  onNetworkChange,
  isOnline,
  isOffline,
  waitForOnline,
} from '@localmode/core';

Events

import { createEventEmitter, globalEventBus } from '@localmode/core';

Errors

import {
  LocalModeError,
  EmbeddingError,
  ModelNotFoundError,
  StorageError,
  QuotaExceededError,
  ValidationError,
  AuditLogError,
  MediaNotSupportedError,
  formatErrorForUser,
} from '@localmode/core';

Testing Utilities

import {
  createMockEmbeddingModel,
  createMockRerankerModel,
  createMockStorage,
  createMockVectorDB,
  createTestVector,
  createSeededRandom,
  createMockHandLandmarkModel,
  createMockPoseLandmarkModel,
  createMockFaceDetectionModel,
  createMockFaceLandmarkModel,
  createMockGestureRecognitionModel,
  createMockLanguageDetectionModel,
  // Framework-agnostic StorageAdapter contract suite for custom adapters —
  // returns Array<{ name, run }>; register with `for (const c of suite) it(c.name, c.run)`
  createStorageAdapterConformanceSuite,
} from '@localmode/core';

Architecture

Zero-Dependency Core

@localmode/core has zero external dependencies. All functionality is implemented using native browser APIs:

  • Vector Search: Custom HNSW implementation
  • Storage: IndexedDB + Memory fallback
  • Encryption: Web Crypto API
  • Sync: Web Locks + BroadcastChannel

Provider Pattern

Provider packages implement core interfaces:

// @localmode/transformers - HuggingFace Transformers.js
import { transformers } from '@localmode/transformers';
const embedder = transformers.embedding('Xenova/bge-small-en-v1.5');

// @localmode/webllm - WebLLM for local LLMs
import { webllm } from '@localmode/webllm';
const llm = webllm.languageModel('Llama-3.2-1B-Instruct-q4f16_1-MLC');

Function-First API

All operations are exposed as top-level async functions:

// Correct: Top-level functions
const { embedding } = await embed({ model, value: 'Hello' });

// Wrong: Class methods
const embedder = new Embedder(model);
await embedder.embed('Hello');

Options Object Pattern

All functions accept a single options object:

const result = await embed({
  model: embeddingModel,
  value: 'Hello world',
  abortSignal: controller.signal, // Optional
});

Structured Results

All functions return structured result objects:

interface EmbedResult {
  embedding: Float32Array;
  usage: { tokens: number };
  response: { modelId: string; timestamp: Date };
}

User Extensibility

Implement any core interface to create custom providers:

Custom Storage

import type { Storage } from '@localmode/core';

class MyRedisStorage implements Storage {
  async get(key: string) {
    /* ... */
  }
  async set(key: string, value: StoredDocument) {
    /* ... */
  }
  async delete(key: string) {
    /* ... */
  }
  async keys() {
    /* ... */
  }
  async clear() {
    /* ... */
  }
  async close() {
    /* ... */
  }
}

const db = await createVectorDB({
  storage: new MyRedisStorage(),
});

Custom Embedding Model

import type { EmbeddingModel } from '@localmode/core';

class MyAPIEmbedder implements EmbeddingModel {
  readonly modelId = 'custom:my-embedder';
  readonly provider = 'custom';
  readonly dimensions = 768;
  readonly maxEmbeddingsPerCall = 100;
  readonly supportsParallelCalls = true;

  async doEmbed(options: DoEmbedOptions) {
    // Your implementation
    return {
      embeddings: [new Float32Array(768)],
      usage: { tokens: 10 },
    };
  }
}

const { embedding } = await embed({
  model: new MyAPIEmbedder(),
  value: 'Hello',
});

Browser Compatibility

Browser WebGPU WASM IndexedDB Workers
Chrome 80+ 113+
Edge 80+ 113+
Firefox 75+ Nightly
Safari 14+ 18+ ⚠️

Platform Notes

  • Safari/iOS: Private browsing blocks IndexedDB → use MemoryStorage
  • Firefox: WebGPU shipped in Firefox 141+ on Windows and 147+ on macOS Apple Silicon — falls back to WASM on Linux/older versions
  • SharedArrayBuffer: Requires cross-origin isolation

Privacy Guarantees

  • No telemetry - We don't track anything
  • No network requests - Core package makes zero network calls
  • Data stays local - All processing happens in your browser
  • Open source - Audit the code yourself

License

MIT