yuimorii26 downloadsGemma 4 inside Obsidian via LiteRT-LM and WebGPU: chat, improve and relink your notes, quiz and flashcards — free local AI. Follows Karpathy's LLM-wiki — ingest into a wiki it maintains (index, tags, links), grounded in your notes. One-time download.
Gemma 4 E4B runs inside Obsidian's own process
and builds Andrej Karpathy's living LLM wiki from your notes.
English · 日本語
⬇ Install from the community store
▶ See what it does · Step through the demo — nothing to install.
Free local AI, with nothing to configure. Gemma 4 E4B runs inside Obsidian's own process through LiteRT-LM and WebGPU — no API key, no Ollama, no LM Studio, no localhost server, no account, no subscription. There is no provider to pick, because the model is already here.
That model then builds Andrej Karpathy's LLM wiki from your notes — a cross-linked layer of cards, concepts, tags, links and a schema you can edit by hand — and every page is shown to you in full before a single byte is written.
Chat with a note or your whole vault, grounded in what you actually wrote and with sources listed by the plugin rather than cited by the model. Turn any note into quizzes and flashcards. Your notes are never uploaded anywhere, because there is no server to upload them to: privacy is a property of the architecture, not a promise in a policy. After the one-time downloads — the ~3 GB model and the WASM runtime, both under Privacy — it never touches the network again.
Sixteen commands, and this is what they add up to. Every row is covered in detail below; the full command list is under Current commands.
| What it does | Touches your notes? | |
|---|---|---|
| Ingest / Scan | One card per note in a separate gemma-wiki/ layer, previewed in full before anything is written |
No |
| Chat | Grounded in one note or the whole wiki, with sources listed by the plugin rather than cited by the model | No |
| ⚡ Skills | Quiz, flashcards and find-gaps on the open note, plus any prompt file you drop in skills/ |
No |
| Concept pages | A page built above every card sharing a tag, linking down into each | No |
| Keep it honest | Review board, provenance spot-check, contradiction sweep, and a model-free tidy pass | No |
| Improve | Rewrites structure and typos, preserving your wording and voice | Yes, always previewed |
| Suggest tags & links | Proposes frontmatter tags and wiki links for the open note | Yes, always previewed |
Two of the sixteen write into a note you wrote, both behind a preview. The rest build and maintain a layer beside your notes and never touch them.

It implements Andrej Karpathy's LLM-wiki pattern: your raw notes are never modified by the model — the immutability constraint binds the LLM, not you, and editing your own notes is the normal fix when a chat surfaces a mistake (the wiki detects the change and offers to re-ingest). A separate, cross-linked wiki layer is built above them — one card per note, and every page is shown to you in full before a single byte is written.
From there: chat grounded in one note or in the whole wiki, quiz yourself on either, build concept pages across everything sharing a tag, and let it flag its own decay with lint, provenance and contradiction checks. Sources are listed by the plugin, never cited by the model — citation is the one thing a small local model would get wrong without anyone noticing.
Everything it writes is plain markdown in your vault — nothing is locked in a database, and nothing needs another plugin to read it back. Its own configuration is notes too: your tag vocabulary and naming rules live in schema.md, where you can edit them by hand and the plugin will obey; every operation is appended to log.md, so you can always see what it did and when; and dropping a markdown file into skills/ adds a command of your own to the ⚡ menu.
Status: 1.0.9, in the community plugin store. The full Karpathy loop is implemented and running; benchmarks below are from real use.
The Karpathy loop — raw notes stay read-only; the plugin maintains a separate gemma-wiki/ layer:
high/med/low confidence written to frontmatter), plus a validated multiple-choice pick of related pages from the index catalog. Everything previews in a review modal; nothing is written without approval. Ingested notes get a badge in the file explorer — pure UI decoration, the note file is untouched.loadPages fills to the budget and stops.gemma — Summarize — The perfect espresso shot so the file explorer shows at a glance both that a model wrote it and what it was about, and recording in frontmatter which model and from which sources — the two things a copy-paste destroys silently. It lands in your folders, under your organisation, and it survives deleting the wiki folder, which nothing the plugin owns is meant to. Being an ordinary note of yours, it is also the one thing a saved answer never used to be: ingestable. Scan it and it becomes a card like any other note — which is the only route by which anything becomes material, because everything the wiki retrieves derives from a note you wrote.cards/ (one card per ingested note) and concepts/ (built across those cards) are in the retrieval set.
Keeping it honest — a wiki nobody reviews is the failure mode this is built against:
source_hash), and staleness. Concept overviews are checked too.
Config as notes — the rules live as plain markdown you can read, edit and version:
schema.md — your tag vocabulary, naming rules, and a rejected list: a tag you deleted by hand does not come back. Tidy the wiki rebuilds the vocabulary from the tags your pages actually use — having local Gemma fold near-synonyms together — and applies it to existing pages. Two separate ticks, each behind its own preview. The rebuild is also a button in Settings. Approving a retag also writes an aliases list — old-tag: vocabulary-tag — so a page still carrying the old name is still recognised as being about the same subject; edit or delete those lines freely, and an alias pointing at a rejected tag is ignored.skills/ — one file per entry in the ⚡ menu. Frontmatter for name/icon/mode, the body is the prompt; a > [!info] callout in the body is documentation and is stripped before the model sees it. fill: true puts the prompt in the input box and waits rather than sending, for a skill that has to be aimed at something. Ships with a README and two examples, which carry a stamp: hash so a later release can improve them unless you have edited the file, in which case it is yours forever.chats/ — a conversation you pressed save on, one file per thread, with its own index.md listing them newest first. Nothing in it is retrieved, indexed, linted or scanned — an answer is output, never input, and a chat sitting in the wiki index would be indistinguishable from a page. It is also the only thing under the wiki folder the plugin cannot rebuild from your notes, so move it out before clearing that folder.Chat panel — shadcn-inspired monochrome, theme-variable driven:
+ attachments — fuzzy-pick any notes as removable context pills, in either mode.skills/. A skill file is frontmatter and a prompt, so the menu holds questions; anything that does something (scan, file a note, reformat one) is a chip above the input instead.Engineering rules the whole plugin follows:
Engine.create rather than assumed from the setting — LiteRT-LM may clamp maxNumTokens, and every budget derived from the request would then overshoot.src/notify.ts): six kinds each with a single meaning, three durations instead of ten hand-picked numbers, and no call site choosing a millisecond count. noop deliberately carries no mark — a command that correctly did nothing is not an event. Failures never print a raw exception; they name the operation, give the first line of the reason, and say where the rest is. Warnings and errors are also appended to log.md, because a toast is not a record.
Click the book-and-spark ribbon icon to open the side panel. Two grounding modes, switched with a pill toggle:
gemma-wiki/index.md catalog first, loads the top-matching ingested pages, and answers only from them (plus the catalog and recent activity log, so meta-questions like "what did I add today?" work too).Either way: answers stream in from a model running inside Obsidian's own process, every answer ends with a deterministic Sources row (clickable — listed by the plugin, not left to the model to cite), per-message copy / regenerate / save-as-note actions, and a save-conversation button in the panel header that keeps the whole thread — the questions included, which are the half you cannot reconstruct from the answers. Both modes refuse honestly when the material doesn't answer a question about your notes, and both will explain a term your material uses instead of repeating it back at you — keeping what the material says separate from what the term means. A + button attaches additional notes as removable context pills; a ⚡ skills menu runs canned single-task prompts (Quiz, Flashcards, Find gaps) against the open note.
Most "local AI" Obsidian plugins still depend on a second, independently-running application — Ollama or LM Studio has to be installed and running in the background before the plugin does anything. That's local in the sense that your notes don't leave your machine, but it isn't local in the sense of "install the plugin and it just works."
This project asks a narrower question: can the model run in the exact same process as the plugin, using only what Electron and WebGPU already provide? The answer, validated end to end (see Benchmarks), is yes — for short, well-scoped text tasks. Whether it's enough to carry a full Karpathy-style wiki (entity/concept extraction, cross-referencing, contradiction detection) is the open question this project is working through, one validated step at a time, in public commit history.
The first row is the only one this project can claim and the others cannot. Most of the rest of the table is the price of it.
| This project | Plugins backed by a provider | |
|---|---|---|
| Where the model runs | Inside Obsidian's own renderer process. There is no second program to install, to start, or to keep running. | In something else — Ollama, LM Studio, a cloud endpoint, a document-conversion service. This holds even for the ones offering a fully-local mode: local there means a daemon you install and leave running. |
| What you set up | Install the plugin. The first run downloads the model, once. | Install the plugin, then obtain the other half: an application to run, or an API key to create and pay for. |
| After the first run | No network calls at all. | A cloud provider is a request per operation; a local daemon is a process that has to be alive at the moment you press the key. |
| Which model | One. Gemma 4 E4B, plus whatever LiteRT-LM comes to support. You cannot bring your own. | Your pick, frontier cloud models included — far more capable than anything that fits in a renderer process. Sixteen or more providers is unremarkable in this category. |
| Mobile | No, and structurally so: it needs WebGPU and ~3 GB on disk. | Yes, wherever the provider is reachable over HTTP. |
| What it can read | Markdown notes. | Markdown, and commonly PDFs, images and Office documents as well — through providers that take files directly, or a conversion backend. |
| Interface language | English. This README is also in Japanese; the plugin's interface is not translated. | Often many. Eleven-language interfaces exist in this category. |
| What it costs to run | ~3 GB of disk, once. | Free against a local daemon; per-token against a cloud provider. |
| The one moving part | No inference API and nothing to configure — but the WebGPU runtime can only be handed multi-gigabyte weights over HTTP, so the plugin runs a loopback server on an ephemeral port to feed itself the model and runtime bytes off your own disk. It binds 127.0.0.1, carries no inference endpoint, and lives only while the plugin is loaded. |
localhost:11434, or a vendor's HTTPS endpoint — an address you configure, and on the cloud side a place your notes are sent. |
None of this says local-in-renderer is better. Gemma 4 E4B inside Obsidian is weaker than a frontier model behind an API, reads fewer kinds of file, speaks fewer languages, and will never run on your phone. What it buys is that there is nothing else to install, nothing to keep alive, and nothing leaving the machine after the first download — and that only matters if the output holds up. Establishing that honestly, including where the approach is weaker, is what the benchmarks below are for.
isDesktopOnly: false form)litert-community/gemma-4-E4B-it-litert-lm model, downloaded once and cached on diskIf you sync this vault, exclude the model file. It is downloaded to
<vault>/.obsidian/plugins/gemma-litert-wiki/gemma-4-E4B-it-web.litertlm— Obsidian gives a plugin nowhere outside the vault to write — so iCloud, Dropbox, and Obsidian Sync with "community plugins" enabled will all replicate ~3 GB to every machine. Each machine can download it once for itself in a fraction of that time. The wiki folder itself is ordinary markdown and should sync normally.Note that
data.json(which holds your knowledge-folder name) often does not sync. If you rename the folder on one machine, the other will not know — it now notices a folder that looks like a wiki and asks before creating a second one, but the cleanest fix is to rename it the same way on both.

Sixteen commands, four habits. Everything else is occasional.
| When | Run | Why |
|---|---|---|
| You wrote or clipped some notes | Scan a folder into the wiki | Drafts a card per new or changed note, review-gated, lowest confidence first. |
| Right after a scan | Tidy the wiki | Ingest coins tags page by page, so a batch fragments the vocabulary and links only point one way. Tidy folds near-duplicate tags into one list and makes links mutual — the completion notice says so when the batch made it necessary. |
| A theme keeps showing up | Build a concept page | Once two or more pages share a tag or mention, this writes the page above them. |
| Every few weeks | Review board, then Provenance spot-check, then Find contradictions | The three ways a wiki quietly goes bad: pages whose notes changed, key points the note never said, and pages that disagree. All three flag; none edits. |
Chat needs no schedule — it reads whatever the wiki holds. And the two commands that write into your own notes, Improve and Suggest tags & links, are run on one note when you want them, never as routine.

All of these are on the command palette (Cmd/Ctrl + P) under Gemma Wiki.
Ask
| Command | What it does |
|---|---|
| Chat with active note (local Gemma) | Opens the chat panel — This-note / Wiki modes, + attachments, ⚡ skills, save-as-note. See above. |
File notes into the wiki
| Command | What it does |
|---|---|
| Ingest this note into wiki (local Gemma) | One strict JSON extraction (summary, 3 tags, 3–5 key points, salient mentions, self-rated confidence) plus a validated related-pages pick from the index — previewed in a review modal, written only on approval. Raw notes are never modified by the plugin; ingested notes get a badge in the file explorer. |
| Scan a folder into the wiki (batch, local Gemma) | The same extraction across whole folders, for new or changed notes only. Opens a dialog that counts what each folder holds and estimates the run time before you commit, remembers your last pick, drafts everything first, then shows one review list sorted low-confidence first. Scope is opt-in: it never sweeps the vault without you ticking a folder. |
| Stop the running scan | Only in the palette while a scan is running; the Scan a folder chip becomes Stop scan at the same time. A model call cannot be interrupted, so the note in flight finishes and is kept — everything drafted so far still goes to the review list, and the rest is offered on the next scan. Stopping never loses work and never writes anything. |
| Suggest tags & links for active note (local Gemma) | Proposes frontmatter tags and links to related wiki pages for one note, behind a preview. |
Build the layer above
| Command | What it does |
|---|---|
| Build a concept page from a tag or mention (local Gemma) | Pick a tag or mention two or more pages share; writes a page above them that links down into each. Member lists self-heal in both directions. |
Keep it honest
| Command | What it does |
|---|---|
| Review board (low-confidence, drifted, and stale pages) | One queue for the three ways a page goes bad: low self-rated confidence, source drift caught by source_hash, and staleness. |
| Find contradictions in wiki (local Gemma) | Checks pages sharing a tag for claims that disagree, recently-changed pairs first. Flags with the reason quoted and never edits. |
| Provenance spot-check (local Gemma) | Traces each key point on a page back to a sentence in the raw note, and flags what cannot be traced. |
| Tidy the wiki (check, then fix what you approve) | The five maintenance commands in one. A model-free check reports orphans, dead index entries, unindexed pages and tag drift; then four repairs — make links mutual, drop dead index entries, rebuild the tag vocabulary, apply it to existing pages — are offered together, ticked where the check found something, and run in turn behind their own previews. |
Write into your own note — the only one that does
| Command | What it does |
|---|---|
| Improve formatting of active note (local Gemma) | Structure, lists and typos only; wording and voice preserved. Long notes are split on headings and blank lines into passes that each fit the context window, rewritten one pass at a time and stitched back byte-exactly; you are told the pass count before it starts. A selection narrows it to one section. Full-result preview before anything is written. |
Setup and diagnostics
| Command | What it does |
|---|---|
| Download model (one-time, ~3GB) | Downloads and caches the model with live progress, instead of waiting for the first command to trigger it. |
| [Test] Check WebGPU | Confirms a usable WebGPU adapter is available. (The four [Test] commands are hidden unless Settings → Model → Developer commands is on.) |
| [Test] Load WASM runtime (no model download) | Loads the LiteRT-LM WASM runtime without the model — isolates runtime issues from model issues. |
| [Test] Fix grammar of selection | Runs a real generation on the selection, logging prefill/decode speed and time-to-first-token to the console. |
| [Test] JSON reliability test (5 runs) | Five independent structured-JSON generations against the selection, reported as a pass rate — the risk test for whether the model can reliably drive the ingest pipeline. |
Every feature, one plate each: gemma-wiki-demo.vercel.app/tour.html — fourteen sections, most important first. Or step through the deck as 23 scenes.
Getting a local model to run inside Obsidian's renderer — instead of a Chrome extension's offscreen document, which is where every existing LiteRT-LM web example runs — surfaced two Electron-specific problems that don't show up in a normal browser tab:
The runtime's loader wants a <script> tag. LiteRT-LM's WASM loader creates a <script src="..."> element to load its Emscripten glue — and file:// scripts are blocked from a non-file:// page by Electron's local-resource guard, so that path could not work here anyway. Fix (v1.0.7): the glue is a UMD bundle and this is a Node-integrated renderer, so the vendor loader is replaced at build time with one that require()s the file off disk. No element is created, nothing is injected into the page, and the path is local and pinned rather than a URL. The 127.0.0.1 loopback server remains, because the .wasm binary itself still has to reach the runtime over HTTP.
The WASM glue misdetects its own script directory. litertlm_wasm_internal.js checks typeof __filename to decide whether it's running under Node.js. Obsidian's desktop app is a Node-integrated Electron renderer, so __filename is defined — but it points at Obsidian's own internal file, not the dynamically-injected WASM script. This hijacks the correct document.currentScript-based path resolution, and the model binary fetch silently falls back to resolving against Obsidian's own page origin instead of the plugin's server. Fix: pre-seed self.Module.locateFile before calling loadLiteRtLm() — the loader checks for this override before running its own (broken, in this environment) auto-detection.
Once loaded, the plugin keeps a single Engine instance alive for the lifetime of the Obsidian session (see ensureEngine() in src/main.ts) so the ~3 GB model and GPU warmup cost are paid once, not per command.
The model runs only inside Obsidian, so two things CI cannot cover are written as repeatable protocols instead: tri-lingual quality (JA/EN/ZH fixtures — Improve must never translate, and CJK must not truncate) and tag-vocabulary reuse (ingest must reuse existing tags instead of coining synonyms). Fixtures are synthetic; pass criteria are explicit.
All numbers are from Conversation.getBenchmarkInfo() — real LiteRT-LM instrumentation, not estimates — measured on real hardware, not vendor-quoted figures.
| Scenario | Prefill | Decode | Time to first token |
|---|---|---|---|
| Cold engine, 2098-token input | 74.7 tok/s | 29.1 tok/s | 28.1 s |
| Warm engine, 1558-token input | 495.0 tok/s | 28.9 tok/s | 3.2 s |
| Warm engine, 274-token input | 386.5 tok/s | 29.5 tok/s | 0.74 s |
| Warm engine, 634-token input (full paragraph) | 494.7 tok/s | 29.1 tok/s | 1.3 s |
Takeaway: the 28-second cold-start figure that shows up on the very first call is a one-time GPU shader-compilation / weight-conversion cost, not a per-call tax — once the engine is warm, prefill throughput jumps roughly 5-6×, and short-selection latency drops to sub-second. Decode speed (about 29 tok/s) is stable regardless of input length or warm/cold state.
Quality, checked by hand against six English test passages (basic typos, subtle grammar, a passage with zero errors, a bulleted list, technical jargon that should not be "corrected", and a 700-word article with errors scattered through the final paragraph): zero missed errors, zero hallucinated changes, exact-match output on the already-correct passage, all technical terms and list formatting preserved, and no quality drop-off between the first and last paragraph of the long passage.
Structured output reliability: 5/5 independent runs produced valid, correctly-shaped JSON ({"summary": string, "tags": string[3]}) with greedy sampling — the load-bearing assumption behind an eventual ingest pipeline.
Implemented — the Karpathy core loop, scoped deliberately to what a small on-device model has been validated to carry:
Everything once listed as next has since landed: user-defined skills (drop a file in skills/), model-assisted contradiction candidates (flagged for human judgment, never auto-fixed), a content-hash dedup gate in scan, the review board, and the community-store listing itself — published, and re-scanned on every release since.
Deliberately out of scope: a multi-provider abstraction layer, image input (the LiteRT-LM web runtime does not support it yet), PDF OCR, and any retrieval scheme more complex than "read the index, then read the pages it points to" — field reports put the flat-index breaking point around ~77 pages, far above this wiki's current size; that decision gets revisited there, not before.
No backend, no telemetry, no analytics. This plugin makes network requests to exactly two hosts, both one-time downloads of things it needs to run, both cached on disk afterwards:
| Host | What | When |
|---|---|---|
huggingface.co |
the Gemma 4 E4B model (~3 GB) | once, when you press Download model |
cdn.jsdelivr.net |
the LiteRT-LM WebAssembly runtime (~20-31 MB), from the pinned @litert-lm/core version this build was compiled against |
once, on first use |
The runtime is fetched rather than bundled because it ships in four variants totalling ~101 MB and your machine loads exactly one, chosen by the library's own feature probes; shipping all four would put a 101 MB payload in every install to use a fifth of it. Only files matching litertlm_wasm_*internal.{js,wasm} are accepted, and nothing here updates the plugin itself — the version is fixed at build time.
The plugin also runs a loopback HTTP server (127.0.0.1, ephemeral port, alive only while the plugin is loaded). It exists because the WebGPU runtime can only be handed the model over HTTP; it serves files from your own disk to your own machine, has no inference endpoint, and is not reachable from outside it.
The WebGPU runtime can only be handed multi-gigabyte weights over HTTP — there is no API that takes a file path. So the plugin serves the model and the .wasm binary to itself on 127.0.0.1 at an ephemeral port, from files on your own disk, for as long as the plugin is loaded.
It carries no inference endpoint and nothing user-supplied ever reaches it: only filenames matching litertlm_wasm_*internal.{js,wasm} are accepted, from the @litert-lm/core version this build was compiled against, fixed at build time. The plugin has no update mechanism of its own.
The runtime's own JavaScript is not loaded over that server. Since v1.0.7 it is require()d directly off disk — a local path, nothing injected into the page — which is both more auditable and what cleared the community directory's automated review.
Nothing else leaves your machine. Your notes, your questions and every generated answer stay on-device: inference, caching and generation all happen inside Obsidian's own process. There is no server to upload them to.