0211cf33f3
Add handwriting OCR pipeline and update documentation - Introduce full OCR workflow: HWR extraction → PNG rendering → GLM-OCR → Ollama refinement - Add new modules for page rendering, GLM-OCR client, style refinement, and ZIP utilities - Update settings with drawj2d path and batch size controls - Document Phase 2 completion and known limitations in IMPLEMENTATION.md - Validate dependencies and handle cross-platform temp directory cleanup ```
9.9 KiB
9.9 KiB
Obsidian reMarkable Sync Plugin — Implementation Summary
Status: ✅ Phase 1 & 2 Complete
| Phase | Status | Description |
|---|---|---|
| Phase 1 | ✅ Done + Reviewed | Core sync, rmAPI bridge, PDF conversion, settings UI |
| Phase 2 | ✅ Done | Handwriting OCR pipeline: HWR extraction → PNG render → GLM-OCR → Ollama style refinement |
🔧 Fixes Applied (Post-Review)
Critical Bugs (all fixed)
| # | Issue | File | Fix |
|---|---|---|---|
| 1 | SyncTracker corrupted plugin settings |
src/sync/tracker.ts |
Merges with existing data before saving |
| 2 | runCommand() hung on spawn errors |
src/utils/process.ts |
Added error event handler + dedup resolution |
| 3 | Invalid "dir" in manifest |
manifest.json |
Removed property |
| 4 | Wrong vault path handling | src/sync/downloader.ts |
Uses vault.adapter relative paths |
Major Issues (all fixed)
| # | Issue | File | Fix |
|---|---|---|---|
| 5 | ls flag order (Go parser) |
src/rmapi/bridge.ts |
--json before positional path |
| 6 | No incremental sync | src/sync/downloader.ts |
Checks modifiedClient vs lastSynced |
| 7 | Unsanitized filenames | src/sync/downloader.ts |
sanitizeFileName() replaces illegal chars |
| 8 | Broken auth flow | src/rmapi/bridge.ts |
isAuthenticated() + user notice |
| 9 | Only root-level sync | src/sync/downloader.ts |
listAllDocuments() recursively traverses folders |
| 10 | Noisy load/unload notices | src/main.ts |
Removed; status bar shows state instead |
| 11 | Broken dev watch script | esbuild.config.mjs |
Uses esbuild.context().watch() |
| 12 | Dead killProcess() code |
src/utils/process.ts |
Removed |
| 13 | mkdir silent failures |
src/sync/downloader.ts |
Wrapped in try/catch with warning |
📁 Project Structure
obidian-remarkable/
├── dist/
│ └── main.js # Built plugin (~15 KB, minified)
├── src/
│ ├── main.ts # Plugin entry: ribbon, commands, status bar, auto-sync
│ ├── settings.ts # Settings tab with validation
│ ├── types.ts # Shared TypeScript types
│ ├── rmapi/
│ │ └── bridge.ts # rmapi CLI wrapper (env, auth check, JSON parsing)
│ ├── sync/
│ │ ├── downloader.ts # Recursive doc listing + incremental download + OCR trigger
│ │ └── tracker.ts # Sync state persistence (merges with settings)
│ ├── ocr/
│ │ ├── pipeline.ts # Orchestrates the 4-stage OCR pipeline
│ │ ├── remarkable-hwr.ts # Extracts built-in HWR text from .rm zip
│ │ ├── glmocr-client.ts # HTTP client for GLM-OCR Server
│ │ └── style-refiner.ts # Ollama client for Markdown cleanup (Option A)
│ ├── convert/
│ │ └── render.ts # .rm page → PNG via drawj2d or rM2svg
│ └── utils/
│ ├── process.ts # Child process runner with error handling
│ └── zip.ts # .rm zip extraction + notebook detection
├── main.ts # Entry point (re-exports plugin)
├── manifest.json # Obsidian plugin manifest
├── package.json # Build scripts
├── esbuild.config.mjs # esbuild config (build + watch modes)
└── IMPLEMENTATION.md # This file
🚀 Usage
Initial Setup
- Install plugin: Copy folder to Obsidian plugins directory
- Install rmapi: Download from https://github.com/ddvk/rmapi/releases
- Install page renderer (optional, for OCR):
- drawj2d: Download JAR, place in PATH or configure
javaPath - rM2svg: Install binary, plus
rsvg-convertor ImageMagick
- drawj2d: Download JAR, place in PATH or configure
- Configure: Open Settings → reMarkable Sync
- Authenticate: Run
rmapiin a terminal once to pair with your tablet
Daily Use
- Sync all: Click ribbon pencil icon or run command "Sync from reMarkable"
- Convert single file: Open a
.rmfile in Obsidian, run "Convert handwriting to Markdown"
🔄 Sync Flow (Phase 1)
User clicks ribbon icon / auto-sync interval
→ isAuthenticated() check
→ listAllDocuments("/") recursively
→ ls --json / (root)
→ For each CollectionType: recurse into subfolder
→ For each DocumentType:
→ Skip if lastSynced >= modifiedClient (incremental)
→ sanitizeFileName() for safe filesystem names
→ mkdir downloadPath (vault-relative)
→ rmapi get → download .rm file
→ (if enabled) rmapi geta → download annotated PDF
→ trackDocument() in sync state
→ (if enabled) trigger OCR pipeline → .md
→ Status bar: "Last sync: HH:MM:SS"
🖋️ Handwriting OCR Pipeline (Phase 2)
flowchart TB
subgraph Stage0["Stage 0: Extract"]
RM[".rm notebook file"]
RM --> UNZIP["unzip .rm archive"]
end
subgraph Stage1["Stage 1: reMarkable HWR"]
UNZIP --> CONTENT["Read content.json"]
CONTENT --> HWR["Extract built-in<br/>handwriting text"]
end
subgraph Stage2["Stage 2: Page Render"]
UNZIP --> PAGES["List .rm page files"]
PAGES --> RENDER["drawj2d / rM2svg<br/>→ page-N.png"]
end
subgraph Stage3["Stage 3: GLM-OCR"]
RENDER --> BASE64["base64 encode images"]
BASE64 --> POST["POST /glmocr/parse"]
POST --> GLM_MD["glmocr markdown_result"]
end
subgraph Stage4["Stage 4: Style Refinement"]
HWR --> MERGE["Merge sources"]
GLM_MD --> MERGE
MERGE --> OLLAMA["Ollama qwen3:32b<br/>Option A: light cleanup"]
OLLAMA --> FINAL["Final .md file"]
end
Stage Details
| Stage | Tool | Input | Output | Notes |
|---|---|---|---|---|
| 0 | unzip |
.rm file |
Extracted directory | .rm files are zip archives |
| 1 | Custom parser | content.json |
Raw text | Best-effort; may return empty string |
| 2 | drawj2d or rM2svg |
.rm page files |
page-0.png, page-1.png, ... |
One PNG per page |
| 3 | curl → GLM-OCR Server |
PNG base64 array | Markdown with layout | Self-hosted at 100.103.83.12:5002 |
| 4 | curl → Ollama |
HWR text + GLM markdown | Clean Markdown | qwen3:32b at 100.103.83.12:11435 |
Style Refinement Prompt (Option A)
You have two sources of text from the same handwritten document:
SOURCE 1 — ReMarkable built-in handwriting recognition (raw text, no formatting)
{hwrText}
SOURCE 2 — GLM-OCR engine output (markdown with structure but possible errors)
{glmOcrMarkdown}
Your task:
1. Merge both sources, preferring SOURCE 2 for structure and SOURCE 1 for text accuracy
2. Fix heading hierarchy (h1 → h2 → h3 logically)
3. Consolidate fragmented paragraphs
4. Detect and format lists, checkboxes, tables
5. Identify emphasized/underlined text and apply bold/italic
6. Preserve all content — do not summarize or remove anything
OUTPUT ONLY THE FINAL REFINED MARKDOWN.
⚙️ Configuration
| Setting | Default | Description |
|---|---|---|
remarkableHost |
https://10.11.99.1 |
reMarkable tablet URL |
rmapiBinaryPath |
rmapi |
Path to rmapi binary |
downloadPath |
remarkable/ |
Vault folder for downloads |
convertToPdf |
true |
Auto-download annotated PDFs |
enableHandwritingMd |
true |
Trigger OCR pipeline after sync |
glmocrServerUrl |
http://100.103.83.12:5002 |
GLM-OCR SDK Server |
glmocrApiKey |
any-string |
Dummy key for self-hosted |
ollamaHost |
http://100.103.83.12:11435 |
Ollama server |
styleModel |
qwen3:32b |
Model for markdown cleanup |
pageRenderer |
drawj2d |
.rm → PNG tool |
javaPath |
java |
Java runtime for drawj2d |
syncInterval |
0 |
Minutes between auto-sync (0 = off) |
📦 External Dependencies
Required (user-provided)
- rmapi binary: https://github.com/ddvk/rmapi/releases
- reMarkable tablet: On local network at configured host
Required for OCR (Phase 2)
- GLM-OCR Server:
python -m glmocr.serveron configured host - Ollama: With
qwen3:32b(or chosen model) pulled - Page renderer:
- drawj2d: Java JAR (recommended for Paper Pro v3.x)
- rM2svg: Binary +
rsvg-convertor ImageMagick
📝 Architecture Decisions
| Decision | Rationale |
|---|---|
| Spawn rmapi as CLI | Mature Go binary; avoids reimplementing reMarkable sync protocol |
| Store auth in vault | RMAPI_CONFIG in .obsidian/rmapi — portable with vault |
Use curl for HTTP |
Available on all platforms; avoids bundling HTTP client |
| Base64 encode images | GLM-OCR server accepts data:image/png;base64,... inline |
| 3-stage OCR pipeline | HWR (free, on-device) + GLM-OCR (accurate) + Ollama (cleanup) |
| Light cleanup (Option A) | Preserves all content; fixes structure without rewriting |
| Template literal prompts | Easy to read and modify; no external prompt files |
🎯 Build & Install
# Build
npm install
npm run build
# Dev (watch mode)
npm run dev
# Install in Obsidian
Copy the `obidian-remarkable` folder to:
- Linux: ~/.config/obsidian/plugins/
- macOS: ~/Library/Application Support/obsidian/plugins/
- Windows: %APPDATA%\obsidian\plugins\
🐛 Known Limitations
- HWR extraction is best-effort: reMarkable v3.x
content.jsonformat isn't fully documented. If no HWR text is found, the pipeline falls back to GLM-OCR alone. - Page renderer path:
drawj2d.jaris assumed in PATH. You may need to set an absolute path in settings (future improvement). - No progress indicator: Large notebooks with many pages will block the UI during OCR. Consider adding a progress modal.
- Temp directory: Uses
.obsidian/rmapi-tmpand cleans up withrm -rf. On Windows,rmmay not exist (needsrd /s /qfallback).
Status: ✅ Ready for testing. Both phases complete and reviewed.