drpwchen/textbook-to-note/figure-remap
Claude Code and agent skill by drpwchen/textbook-to-note
- community
Checking that the SKILL.md file is still in the repository.
Extract single figures from PDF textbooks on-demand with built-in QC verification. Primary use: when writing/supplementing a note that needs an embedded figure (anatomy, classification, algorithm, imaging). Each call checks an existing fast-path crop, re-extracts from PDF if missing/wrong, retries with local-vision guidance, and escalates to a frontier-model vision read only if all else fails. Also contains legacy batch tools for whole-book re-extraction. Trigger: "extract figure", "figure for note", or proactively when a note's TODO/REF references a figure not yet embedded.
Install
npx vskill@latest install drpwchen/textbook-to-note/figure-remapSKILL.md
figure-remap — deterministic document-grounding system
This skill calls scripts in your clone of the textbook-to-note repo. Atinstall time, replace{REPO}below with the absolute path of the clone.
Architecture (read this before implementation details)
figure-remap is a multi-backend deterministic document-grounding system, not a "crop utility." The system has six explicit layers:
Future backends plug in at the Backend layer with their own capability check and a function returning bbox + ambiguity flags — nothing above changes.
==Architectural freeze rule==: do not add per-book heuristics or if scanned/elif born-digital branches into the gate function. New behavior goes into a new backend plus a new capability check in _select_backend.
Backends — when each is used
_select_backend() routes pages by structural capability, not by document type:
| Category | Signature | Backend | |
|---|---|---|---|
| Pure born-digital | ≥2 separable rasters, no full-page background, native fonts | geometric (0.95 confidence) | |
| Born-digital text page | 1 small raster + many text blocks + embedded fonts | geometric (0.75 confidence) | |
| Hybrid scan (near-full-page raster + embedded figure rasters + OCR overlay) | e.g. a scanned reference book with an OCR text layer laid over the scan | caption_anchor (0.92 confidence) | |
| Pure scanned | Near-full-page raster only, no embedded figures, sparse OCR text | caption_anchor (0.9 confidence) |
The hybrid-scan category is the one that surprises naive backend selection: the PDF library reports multiple "assignable" rasters (the embedded figure photos), but the surrounding OCR-overlay text contaminates the geometric backend's text-bleed QC check. Detecting the page-background raster and routing to caption-anchor is the fix.
Scanned/hybrid backend — caption_anchor
Module: figure_scanned.py. Algorithm:
- Render the page at 200 dpi to a cache file (keyed by PDF hash + page +
dpi).
- Walk the page's text-with-position data for
Fig X-Ycaption text
bounding boxes (with an inline-reference filter — must start at a line beginning, not mid-sentence after a word).
- Apply Class A OCR normalization (silent, 1-to-1): common OCR
confusions (l/I → 1, S → 5, O → 0) only inside the numeric portion of the fig_id.
- Build a caption-match object per caption with an alias set.
- Look up the target fig_id across all caption alias sets:
- 0 matches →
L1_no_caption_matchhard_fail - ≥2 matches →
L1_target_matches_multiple_captionshard_fail
- Infer page-global caption direction (caption-below is the layout
default).
- Compute the figure region: the area opposite the caption, bounded by the
previous/next caption in the same column.
- Crop the rendered page; run an ink-density trim to tighten the bbox.
- Scanned QC: whitespace-fill check only (text-bleed is meaningless on
scanned PDFs — the OCR overlay covers the whole page).
- On hard_fail: write a sibling
.fail.jsondebug artifact with policy
version, captions, columns, backend reasoning.
Known Phase-1 limitations (deferred):
- Class B OCR ambiguity (e.g.
4-28vs4-2B) currently fails
L1_no_caption_match for OCR-mangled targets.
- L2 ownership-overlap guard, L3 direction-quality guard not yet built.
- Direction inference is page-global, not per-column.
fail.json schema
Every scanned-mode hard_fail writes a sibling <out>.fail.json debug artifact, with a versioned schema. Stable fields:
| Field | Type | Meaning | ||||
|---|---|---|---|---|---|---|
schema_version | string | bumps only on breaking schema changes | ||||
policy_version | string | policy that produced this fail | ||||
fig_id_target | string | the normalized fig_id the caller asked for | ||||
fail_reason | string | reason code (e.g., L1_no_caption_match) | ||||
page_idx | int | 0-indexed page | ||||
pdf_sha1 | string | first 12 hex chars of PDF hash | ||||
backend_selected | string | "geometric" or "caption_anchor" | ||||
backend_confidence | float | 0..1 | ||||
backend_reasons | list[string] | one-line tokens explaining the choice | ||||
captions | list[object] | each: {raw, aliases, ambiguity, bbox} | ||||
columns | list[[x0,x1]] | column boundaries in PDF points | ||||
direction | string \ | null | "above" \ | "below" \ | null | |
render_path | string | path to the cached rendered page image |
Downstream consumers must check schema_version and refuse to parse unrecognized majors.
Performance helper — extract-page for scanned books
When a workflow needs all figures on one scanned page, use the batch CLI subcommand instead of N independent extract calls:
==This is NOT a contract addition== — extract-page is a render-cache-aware loop that calls extract() once per fig_id. Each emitted JSON line is a full single-figure contract dict; workflows can substitute N independent extract calls without behavior change — only speed differs.
Entrypoint contract (call THIS, not the gate)
There is ONE sanctioned entrypoint for every figure request — figure_remap.py extract. Workflows MUST go through it and MUST NOT call the internal gate script directly. The entrypoint is the abstraction boundary: it returns a stable minimal contract and hides the implementation (matching method, strict mode, fallback policy, QC ladder), so logging stays uniform and policy is enforced in one place.
Source modes
--source selects where the figure comes from. Default is auto — callers don't need to think about a pre-extracted fast path; the entrypoint resolves it from the book convention <OUTPUT_DIR>/<book>/figures/Fig_<id>.* automatically.
--source | Behaviour | When to use | |
|---|---|---|---|
auto (default) | (1) Try book-convention pre-extract; QC-gate it. (2) On miss/QC fail → fall through to strict geometric match + ±1 page sweep. | Normal note-writing. | |
pdf | Skip the existing fast path entirely; ignore even an explicit --existing. Always re-extract from PDF. | Validating a code change, suspect a pre-extract is contaminated. | |
existing | Cache-only: hard_fail if the convention path is missing or QC-fails. No PDF fallback. | Trust-the-batch mode after offline validation, or when the PDF is unavailable. |
Contract returned (and ONLY this — never the gate's internal shape):
Those eight keys, no more and no fewer — _validate() in figures/figure_remap.py raises on any extra or missing key.
qc_degraded/qc_skipped: one of the two BLOCKING checks could not run
(e.g. the source page render was unavailable), so a pass here is weaker than a fully-gated one. Treat a degraded pass as "embed, but eyeball it". A skipped advisory check never sets this — it was never gating anything.
match_quality:exact= deterministic geometric match ·uncertain= a
fallback crop (only with --no-strict) · failed = no crop. ==Branch on this, never on the engine method.== The precise method is logged only in a local QC log, deliberately kept out of the contract.
status:pass(exit 0) →fileis a QC-passed crop; embed it.
==Embed the `file` string verbatim — never the `--out` you asked for, never a name rebuilt from the `Fig_{id}_{Book}` convention.== file is the authoritative path and it legitimately differs from --out: the auto fast path returns the book's pre-extracted image, so a template saying .png can come back .jpeg. A rebuilt name is derived from the real one, so it reads correct in review and only a machine catches it. Prove it: ``bash python {REPO}/figures/figure_embed_lint.py --notes-dir {NOTES_DIR} "<note.md>" `` exit 0 clean · 1 = MISSING (a guessed filename, or an embed written for an extract that actually failed) or CASE MISMATCH (opens on Windows/macOS, breaks on git and Linux) · 3 = notes dir not found = unverifiable, not a pass.
status:fail(exit 1) → deterministic miss / hard_fail. ==NOT a wrong
figure — a correct refusal.== Fix --page, escalate to vision, or leave a <!-- TODO -->.
status:failwithreasonstartingpregate=(exit 1,hard_fail:false)
→ different animal: extraction and QC both succeeded, then the junk pre-gate (figures/pregate.py) judged the crop to be a chapter-title banner or a blank. ==Skip this fig_id entirely — do not retry, do not fix --page, do not leave a retry TODO.== Retrying only re-crops the same banner. Applies to born-digital geometric crops; the scanned caption_anchor path and the existing fast path return no bbox, so the pre-gate abstains there.
status:escalate(exit 2) → non-strict only; read the page render, then
re-call with --bbox.
Default is strict (deterministic). --no-strict re-enables the legacy-compatibility fallback ladder, which can yield a plausible-but-wrong crop — avoid for note-writing.
⚠️ A pass guarantees the right raster for the fig_id, not that thefigure depicts what you assume. Still read the real caption text beforetrusting its content.
Policy version
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/figure-remap/SKILL.md
Popularity
- Repository stars
- 104
- Repository forks
- 25
- 7-day trend score
- 23
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/drpwchen/textbook-to-note/figure-remap)