Johannes Kaindl348 downloadsTranscribe the images and PDFs in a note to Markdown with a local vision LLM — fully offline, non-destructive, streaming into the sidebar.
🇬🇧 English · 🇩🇪 Deutsch
Transcribe images and PDFs in a note to Markdown with a local vision LLM — fully offline, non-destructive, streaming live into the sidebar.
type value) can be remapped to your own vault schema, consistently across transcript, PDF and description notesImage to Markdown turns the embedded images and PDFs of an Obsidian note — scans, screenshots, photographed pages — into editable Markdown using an OpenAI-compatible vision model that runs on your own machine. Nothing leaves your computer, and your source note is never overwritten: each image or PDF gets its own transcript note, and the original embed is simply replaced by an embed of that new note.
scan-text, label "Image → Markdown") opens the "IMG → MD" view. It lists every embedded image of the active note as a checkbox list — all preselected, with unsupported formats disabled. The "Transcribe" button streams the vision model's answer live into one card per image, including an expandable thinking block for reasoning models and a copy button. Each card has a "Create note" button, plus a footer "Apply" button once at least one card is done. Cards are read-only and show the raw Markdown pre-wrapped. After a transcript is written, the handled image drops out of the list on the next scan.pdfMaxPages) and a mobile-friendly render scale (pdfRenderScale, a 1.0–4.0 slider) keep memory usage in check, and the page separator in the merged note (pdfPageSeparator) is configurable. No external CDN — pdf.js is bundled fully offline.app.fileManager.getNewFileParent) because there is no source note to sit next to. The frontmatter contains no source_note field; source_pdf/source_image, created, transcribed_by (and pages for PDFs) are still written. The source file is never modified. Idempotency and override apply as usual.source_pdf or source_image frontmatter field that resolves to the source file, the sidebar shows "✓ transcript exists" with an "open" link for that entry instead of re-transcribing. Such entries start unchecked; re-tick the row's checkbox and transcribe to force a new transcription (the row's tooltip reads "re-transcribing overwrites it") — the existing note is overwritten while its full frontmatter (except transcribed_by/pages) is preserved. Overwriting itself goes through the diff-confirm step described next.+/- markers are readable without relying on color (accessible for red-green color blindness). In-session retries — e.g. filling in a PDF page that failed the first time — skip the prompt entirely, and the gate compares the note's actual on-disk content, so it still fires correctly if you hand-edited the note between two overwrites in the same session.data.json therefore works on all your devices: put localhost:1234 first (the machine running LM Studio), then a LAN IP as fallback (reachable from phone/tablet via WireGuard). Migration is automatic: an existing visionEndpoint key in data.json is silently promoted to visionEndpoints — no manual action needed.type value itself — can be remapped to your own vault's schema under "Frontmatter mapping" in settings (e.g. kind → type), consistently across transcript, PDF and description notes. (Renaming existing notes vault-wide is planned for a later release.)transcribe-active-note) — batch transcription without opening the sidebar.open-sidebar) — opens the sidebar view.scan-text) — transcribes only the image under the cursor. (PDFs are not supported via context menu — use the sidebar.)Reasoning models that emit reasoning_content in the stream, or inline <think> tags, get their thoughts collected into the expandable thinking block. Reasoning is ephemeral — it is shown to you but never added to the LLM history.
Search for Image to Markdown in Settings → Community plugins → Browse, then click Install and Enable.
Download main.js, manifest.json, and styles.css from the latest release and place them in <vault>/.obsidian/plugins/image-to-markdown/, then enable the plugin under Settings → Community plugins.
git clone https://codeberg.org/jkaindl/image-to-markdown
cd image-to-markdown
npm install
npm run build # produces main.js
Then copy main.js, manifest.json, and styles.css into <vault>/.obsidian/plugins/image-to-markdown/ and reload Obsidian.
Prefer to skip the sidebar? Run the command "Transcribe images in the active note" to batch-transcribe the active note. Or right-click an image in the editor and choose "Image → Markdown" to transcribe only the image under the cursor.
Open Settings → Community plugins → Image to Markdown. The settings live under the heading "Vision (Image → Markdown)".
| Setting | What it does | Default |
|---|---|---|
| Vision endpoints | Ordered list of OpenAI-compatible servers. The plugin pings them in order and uses the first reachable one. | ["http://localhost:8080"] (the MLX default — note that LM Studio uses :1234) |
| Vision model | The vision-capable model to use (e.g. Qwen2-VL, Llama-3.2-Vision). A dropdown filled from the endpoint's /v1/models; if the endpoint is offline it becomes a free-text field. |
"" (empty) — the model actually used is read from response.model |
| Vision prompt | The instruction sent to the vision model; freely editable text area. The shipped default is localized (English or German, following Obsidian). | "Transcribe the text in the image exactly to Markdown. Preserve the structure: headings, paragraphs, emphasis, lists and tables. Output only the Markdown, no comments." |
| PDF max. pages per run | Safety cap on the number of transcribed PDF pages per run — larger PDFs must be narrowed via the page range. Hard-capped at 500. | 25 |
| PDF render scale | Render resolution of PDF pages before OCR (1.0–4.0 slider, step 0.5). Low = faster, less memory; high = sharper page images & better OCR on small text (2.0 ≈ 144 dpi). On mobile it is clamped to 1.5 to protect against OOM. | 2.0 |
| PDF page separator | How pages are separated in the merged transcript note. Five options: Obsidian comment %% Page N %% (hidden in reading view), heading ## Page N, horizontal rule ---, page break (HTML, for export), or none (seamless text). |
Obsidian comment %% Page N %% |
| Use embedded PDF text | For born-digital PDF pages with a real text layer, send the exact text to the model to format as Markdown instead of OCR-ing a rendered image — faster and without OCR errors. Scan/figure pages fall back to the vision model. | On |
| Expand thinking by default | Whether each refinement round's thinking (reasoning) block starts expanded. Off = it collapses once the model finishes thinking (you can still open it by hand). | Off |
| Description categories | The fixed set of categories offered to the model in Describe mode (one per field, add/remove like the endpoint list). | A built-in list, editable |
| Frontmatter mapping | Remap every frontmatter key written into generated notes — and the type value — to your own vault schema (e.g. kind → type), consistently across transcript, PDF and description notes. Change with care: existing notes keep their old keys (a vault-wide migration is planned). |
The plugin's default keys |
Endpoint tip: enter the base URL without a trailing /v1 — the client appends /v1 itself. (normalizeEndpoint strips a trailing /v1 and slashes, so both forms are accepted; a doubled …/v1/v1/… path would otherwise silently return an empty transcript.)
Next to the input fields the settings tab shows a connection status indicator with a "Test connection" button, and a "Vision capability" row with a "Test vision" button that confirms whether the selected model can actually read images — see the manual reference.
For each selected image, the plugin builds a multimodal chat-completions request — the image lives in the content array — to the configured OpenAI-compatible vision endpoint, and streams the Markdown back over SSE (content plus reasoning_content). It then writes one transcript note per image with a transcribed_by frontmatter field (the model name taken from response.model, because some servers such as LM Studio ignore the request's model field and use the loaded model instead) and replaces the image embed in the source note with an embed of the new note. The result is non-destructive and idempotent.
For PDFs, each page is rendered to a canvas by the bundled pdf.js (offline, no CDN; worker embedded as a Blob URL), converted to a PNG data URL, and sent to the same vision endpoint as a regular image. Pages stream as individual cards in the sidebar, and one transcript note is produced for the whole PDF.
The architecture and module layout are documented in AGENTS.md.
The full documentation follows the Diátaxis framework — see docs/manual/index.md:
See the changelog for release notes.
Sent to the model: PNG, JPG, JPEG, WebP, GIF. Recognized but skipped (with a notice): BMP, HEIC, HEIF. HEIC/HEIF is the iOS default and is rejected by vision models — set iOS to "Most Compatible" ("Maximal kompatibel") or convert the image first. When an image is skipped you'll see a notice such as: Format .heic nicht unterstützt (HEIC? iOS auf „Maximal kompatibel").
vault-rag — the sister plugin, home of the RAG core (related notes, semantic search, chat). Image to Markdown was split out of vault-rag 0.2.0 on 2026-06-21 because image transcription is not RAG; the two only ever shared the SSE transport.
Contributions are welcome. Please read CONTRIBUTING.md for the workflow (test-driven, main always green, feature work in feat/<name>, Conventional Commits) and AGENTS.md for the architecture and module conventions. The canonical repository lives on Codeberg; GitHub (johannes-kaindl/image-to-markdown) is a mirror.
Copyright © 2026 Johannes Kaindl.