DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Ab Ocr — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins

dsh-ab-ocr

Ab Ocr

--- description: "外部 ocr 工具:逐页识别扫描的 PDF 或图像,仅在处理完最后一页后按顺序合并页面,并使用模型确定的标题级别保存为一个 Markdown 文档。" kind: "package-reference" ---

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add dsh-ab-ocr@0.1.19
README兼容性版本

兼容性与来源证明

Ab Ocr 以 dsh-ab-ocr 发布,当前版本为 0.1.19。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
any
发布来源
npm
Registry 更新时间
2026/10/16

版本

0.1.19stable
2026/10/13
0.1.18stable
2026/10/11
0.1.17stable
2026/10/11
查看其余 17 个版本收起版本
0.1.16stable
2026/10/11
0.1.15stable
2026/10/11
0.1.14stable
2026/10/9
0.1.13stable
2026/10/9
0.1.12stable
2026/10/9
0.1.11stable
2026/10/9
0.1.10stable
2026/10/9
0.1.9stable
2026/10/4
0.1.8stable
2026/10/3
0.1.7stable
2026/10/2
0.1.6stable
2026/10/2
0.1.5stable
2026/10/2
0.1.4stable
2026/9/28
0.1.3stable
2026/9/28
0.1.2stable
2026/9/26
0.1.1stable
2026/9/25
0.1.0stable
2026/9/25

相关插件

正在加载相关插件…

最新版
0.1.19
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
108.7 MB
文件数
69
Surface
any
许可证
MIT
发布源
npm
GitHub
★ 250.9k
周下载
1,494
最近提交
2026/10/15
查看源码 ↗项目主页 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 vision-media 分类下经过校验的插件。

Experimental Voice Input Bundle@deepseek-ai/dsh-experimental-voice-input-bundle实验性语音输入,使用本地 SenseVoice;首次使用时下载其运行时Modlens@liustack/modlens面向仅支持文本的 LLM 的插件视觉能力,由免费的 Antigravity CLI 提供支持Tool Describe Image@linxin666/dsh-tool-describe-image面向模型的 describe_image 工具,用于 dsh Web GUI:通过在兼容 OpenAI 的端点调用视觉语言模型,为文本模型提供图像理解能力,以描述一张图像(本地路径、http(s) URL 或附件引用)。可热插拔 —Vision Toolkit@anionex/dsh-vision-toolkit面向 Harness 原生集成的 DeepSeek 与 agent-vision-toolkit:图像问答、OCR、定位、UI 还原、像素差异、Artifacts 和 Web UI。

README


description: "The out-of-tree ocr tool: recognizes a scanned PDF or image page by page, merges the pages in order only after the last one, and saves one Markdown document with the heading levels the model decided." kind: "package-reference"

@deepseek-ai/dsh-ab-ocr

Summary

dsh-ab-ocr gives this machine's dsh profiles one way to read a document whose text cannot be selected. One ocr call renders, recognizes, and merges every page of a PDF or a still image, and saves the result as one Markdown document: page numbers and repeated edge lines removed, the whitespace a line box leaves behind removed, and — by the package default — paragraphs rejoined across page breaks. This deployment sets joinAcrossPages/joinWrappedLines to false, so it preserves the recognized format and keeps each page and each paragraph separate instead (约束八:不合并段落和上下页).

The outline is a two-step shape. The merge infers a heading level for every line it can, and writes the lines a reader might call headings to a small outline file beside the document. The conversation's model reads that file, decides the levels, and calls ocr again with a levels value; the second call rewrites the document from the geometry it stored, without recognizing anything again. With no second call the document is still complete — it simply carries the inferred outline.

The OCR engine is RapidOCR, whose PP-OCRv6 detection, classification, and recognition weights ship inside the wheel, so installing the dependencies is installing the model. The environment is created from inside the package by pnpm run setup.

Table of Contents

  • Use this package
  • Install the engine
  • Configuration
  • What one call records
  • Where the files go
  • Understand the implementation
  • Model Experience
  • Evidence
  • ./invariant
  • Known Limitations and Deferred Work
  • Dev Note

Use this package

Mount the row in a profile when the agent should read a scan, a photographed page, a screenshot, or a PDF whose text cannot be selected. The row injects ctx.tools and ctx.systemPrompt: the tool reaches the model catalog through the ordinary schema assembly, and the prompt through the ordinary section assembly. Mounting it adds no provider — every file it writes goes through the filesystem capability the composition already has.

Mount it in a profile

# <DSH_HOME>/profiles/<profile>/cordis.patch.yml
# The package is out-of-tree: its own bundle patch (./cordis.patch.yml) already
# inserts the `ocr` row, so the profile only supplies the config. Do NOT re-insert
# the row here — a second `- insert: id: ocr` would register it twice and the
# harness would warn "tool ocr is already registered" at startup.
- id: ocr
  config:
    dpi: 200
    maxPixels: 12000000
    maxPages: 2000
    maxDocuments: 20
    timeoutMs: 1800000
    startupTimeoutMs: 60000
    maxOutlineCandidates: 400
    engineLifetime: perDocument
    writePageFiles: true
    outlineCandidateRatio: 1.05
// <DSH_HOME>/profiles/<profile>/package.json  (written by `dsh plugin add dsh-ab-ocr` — a semver range, never link:)
"dsh-ab-ocr": "^0.2.0"

Every bound that block states is required: the row is where this deployment says how much work, memory, time, and disk it is willing to carry, and a row that omits one fails to load naming the field. The recognition heuristics have documented defaults and appear here only when this deployment wants a different value.

The package is installed from npm only — never link: or file:. dsh plugin add dsh-ab-ocr writes a semver dependency range into the profile's package.json, and pnpm install resolves it from the registry; the bundled installer (dsh_install_dsh_ab_plugin.py ocr) runs both steps and then states the deployment config row above. A link: dependency would also be rejected by npm publish, so it is not a supported install path.

Run pnpm install in the profile directory after editing either file.

Install the engine

The Python environment is separate from pnpm and is created by a script inside this package:

pnpm --dir <plugins root>/plugins/dsh-ab-ocr run setup

It finds a base interpreter, creates python/.venv, installs python/requirements.txt, runs the worker's own --self-test, and reports the interpreter, the package versions, and the model files the wheel carries. It is idempotent: a run against a working environment verifies and exits without touching the network.

FlagEffect
--python <path>Base interpreter to build the environment from (or DSH_OCR_PYTHON)
--prefetchAlso load the engine once, so the first recognition is not the one that waits for the models
--forceRemove the environment and build it again
--jsonPrint the summary as one JSON object

Installing into the environment needs permission to create directories with a private mode, so run it outside a restricted shell. The tool reports what is missing — and names this script — when a call arrives without an environment.

Configuration

Every field is overridable from the row. A field the deployment owns is required — it has no value inside the package, because a hidden default would state an agreement the deployment never made — and a field whose default is a documented resolution rule or a recognition heuristic is defaulted.

FieldRequiredDefaultMeaning
pythonPath(auto)Interpreter carrying the OCR dependencies
workerScript(auto)Worker script to run
outputDir(beside source)Where the Markdown files go
pageDirName.ocr-pagesPer-document directory, relative to each output file
writePageFilesyes—Write each page's text to its own file as it finishes
dpiyes—PDF render resolution
maxPixelsyes—Ceiling on one rendered page; 0 disables it
textScore0.5Recognition confidence floor
detectHeadingstrueRender the inferred outline into the document
headingMinRatio1.18Glyph-height ratio that makes a line a heading
indentRatio1.0Left-edge offset, in body glyph heights, that marks an indented first line
paragraphGapRatio0.85Vertical gap, in body glyph heights, that separates two paragraphs
outlineCandidateRatio1.05Glyph-height ratio above which a line becomes an outline candidate
maxOutlineCandidatesyes—Most candidates one document's outline file may list
detectColumnstrueSplit a page at a vertical gutter
removePageNumberstrueDrop folios
removeRunningHeadstrueDrop lines repeated in the edge bands

The required group is what the tool may cost this machine: the render resolution, the per-page pixel ceiling, the page and file ceilings, the two time ceilings, the outline candidate ceiling, the engine lifetime, and whether per-page text is kept. The defaulted group is what a document reader wants: where the interpreter, the worker, and the artifacts are found, and how a page is read and merged.

startupTimeoutMs exists because the startup check is a separate process from the batch. An environment whose engine never loads has to be refused inside the deployment's startup window rather than held for the whole recognition ceiling.

What one call records

// ocr({ path: "D:/docs/部署规范.pdf" })
{
  "documents": [{
    "input": "D:/docs/部署规范.pdf",
    "output": "D:/docs/部署规范.md",
    "pages": 3, "totalPages": 3, "selection": [1, 2, 3],
    "lines": 16, "headings": 3, "chars": 289,
    "droppedPageNumbers": 1, "droppedRunningHeads": 0, "joinedAcrossPages": 0,
    "pageDir": "D:/docs/.ocr-pages/部署规范",
    "digest": "9c41acae7ce7",
    "outlinePath": "D:/docs/.ocr-pages/部署规范/9c41acae7ce7/outline.json",
    "outlineTruncated": false,
    "corrections": 0,
    "seconds": 31.56
  }],
  "failures": []
}

A call carrying levels reports corrections instead of a recognition, and rewrites the Markdown the earlier recognition produced. A document that fails does not discard the others; each failure carries its own path and reason.

Where the files go

For a source named <stem>.<ext> under a base directory <base> — the source's own directory, or outputDir:

ArtifactPath
Markdown<base>/<stem>.md
A second recognition of the same source<base>/<stem>.<digest12>.md
Per-page text<base>/.ocr-pages/<stem>/<digest12>/page-00001.txt
Recognition record<base>/.ocr-pages/<stem>/<digest12>/record.json
Outline sheet<base>/.ocr-pages/<stem>/<digest12>/outline.json
Most recent recognition<base>/.ocr-pages/<stem>/latest.json

The source's extension is replaced by .md and every space is removed, so the name is space-free on disk and in a link. Tabs, non-breaking spaces, and the full-width space are removed with it; characters a file system rejects are replaced, a reserved Windows device name gains a leading underscore, and a name that compacts to nothing becomes document.

digest12 is the first twelve hex characters of a hash over the source path and content, the page selection, and the merge settings. The worker reports the source's own content digest as the document starts, so the name follows what the document is rather than how large it happens to be: a source edited without changing size is a different recognition, and an unchanged one is always the same recognition. That is how a levels call finds what to correct without scanning.

<stem>.md is never overwritten with different content: when it already holds another document, the new one takes the digest name. Repeating a recognition whose result is unchanged reuses the file it wrote. writePageFiles: false suppresses only the per-page text; the record and the outline are always written, because the correction pass reads them.


Understand the implementation

Design commitments

  • One page in memory. A page is rendered, recognized, reported, and dropped before the next page is rendered.
  • A page is saved as it finishes. The text is written when the page event arrives, so an interrupted run keeps every page it completed.
  • The document is merged only after the last page. The merge is a separate pass in the worker, over records it kept; nothing is written to a document while a page is still being recognized.
  • The engine is released per document. engineLifetime: perDocument rebuilds it for each document, so a batch never holds more than one.
  • Nothing reads a PDF's text layer. Every page is rendered and recognized, which is what makes a scan work and what costs time on a text PDF.
  • Every write goes through ctx.fs. See Why every write goes through ctx.fs.

Why every write goes through ctx.fs

This deployment enforces its file policy in two different places, and only one of them reaches a plugin that opens files itself.

LayerWhat it confines
The process sandboxA command started through the sandbox provider, and everything that command spawns
ctx.fsEvery mutation a plugin performs through the capability, at the path level

The base bundle mounts the sandbox-enforcing filesystem provider, so ctx.fs.writeText fences each write against the session mode and the writable roots. A plugin that calls the host filesystem directly is fenced by neither. The worker therefore opens only the document it was given and writes to stdout; the page text, the geometry, the merged Markdown, and the outline all travel back as events, and this module writes them through ctx.fs.

The capability is read with ctx.get('fs') when a call runs and is not listed in the row's inject. Cordis satisfies a declared injection by waiting for the service, so declaring fs would make the row a provider requirement and leave it unmounted in a composition that has none.

Why every write carries a fence

A sandboxing backend fences a mutation by a per-call policy, and that policy — not the backend's own default — is the only thing naming the workspace the calling session runs in. Calling writeText without one makes the backend fall back to the deployment's root, which is the directory the server was launched from; a session working anywhere else then has every artifact refused, one page at a time. src/sandbox.ts owns that seam:

  • callFence(ctx, request) resolves the calling session's policy once per call and refuses a composition that mounts a confining backend with no ctx.sandboxPolicy.
  • saveText(fs, target, content, fence, signal) is the one path every persisted text goes through, so the fence is applied in one place. Every other write in the plugin (writeJson, writeDocument, the page files, the correction pass) delegates to it.
  • The resolved fence's workspaceRoot is also the base a relative outputDir resolves against, so a call that names no directory writes inside the only root the fence will accept. process.cwd() is not consulted at all when a policy is available.

The guard has to live at the call rather than at load: dsh-fs-sandbox declares sandboxPolicy as a declared injection, so in a composition that mounts the backend without the policy service the backend is never constructed — ctx.get('fs') is undefined while this row applies, and the row mounts cleanly. The first call is the earliest point at which the composition's real shape is knowable.

A refused write is restated rather than passed through: the denial marker, the path, the mode, and the workspace root the artifact has to move under, with the backend's refusal kept as the cause and the FS_SANDBOX_DENIED code re-stamped, so a caller that keys a retry off the code still sees it.

Source map

PathRole
src/index.tsplugin entry: identity, routing section, tool registration, pending-call presentation
src/config.tsthe deployment's bounds and the recognition heuristics, and their schema
src/plan.tsrequest planning: interpreter, worker, and artifact resolution; the worker spec
src/recognize.tsone call: startup check, the batch, the correction pass, the artifact writes
src/events.tsthe worker's event stream and the per-call state it accumulates
src/documents.tsthe artifact files of one recognition, and the report it produces
src/artifacts.tsartifact paths and the recognition digest
src/records.tsthe stored record and outline formats, their readers, and the JSON write
src/sandbox.tsthe per-call fence, the guard for a composition that cannot resolve one, and the one policy-stamped write path
src/levels.tsthe levels argument
src/filename.tsoutput naming
src/worker.tsprocess transport: spec handoff, line framing, timeout, cancellation
src/render.tsmodel-facing result text
scripts/setup.mjsthe environment installer
python/ocr_worker.pyworker entry: engine lifetime, page loop, merge and assemble modes
python/source.pyPDF and image page sources
python/layout.pyboxes to lines, column detection, outline levels
python/clean.pyfolios, running heads, line joining
python/assemble.pythe merge pass, the candidate list, and the level overrides

The per-page pass

For each page the worker renders the page, recognizes it, and turns the engine's boxes into reading-order lines. It reports the page's text and its geometry on a page event; this module writes the text to page-NNNNN.txt and accumulates the geometry. The page's pixels are dropped before the next render. A page split by a vertical gutter is read column by column; every other page is read top to bottom, with boxes that share a row joined left to right.

The page files carry five digits, so their order on disk is their order in the document past the 9999th page.

Page order and identity

The page selection is resolved to the explicit list of page numbers it names — "1-5,8" is 1, 2, 3, 4, 5, 8, and nothing else — and the worker emits pages in that ascending order with the source's own page number as the index. The merge looks each page's lines up by that number rather than by position, so a selection that does not start at page 1 merges like any other.

This module validates what the worker reports: a page number must be a positive integer, strictly greater than the one before it, and within the document's own page count. A violation aborts the call with a message naming the page, rather than producing a document with pages out of order or missing.

The merge pass

When the last page is done the worker merges its records in one pass.

  • Outline. Heading levels come from the trailing outline number (1.1, 第 3 章, 三、), from a named section (摘要, References), or from glyph height measured against the document's own body size.
  • Furniture. Folios and repeated edge lines are removed.
  • Whitespace. Blank runs collapse, spaces that exist only because of a line box are removed between CJK characters, and a Latin word split by a hyphen at a line break is rejoined.
  • Page breaks. A paragraph cut in half by a page break is rejoined when the page's first kept line continues it — but only when joinAcrossPages is true (its package default). joinAcrossPages: false closes the paragraph at the page edge and starts a fresh one on the next page, so the upper and lower pages are never stitched (约束八:不合并上下页).
  • Per-line fidelity. When joinWrappedLines is true (its package default) a wrapped continuation line is space-joined into the paragraph it continues; when false, each recognized physical line is kept as its own line and paragraph grouping is carried entirely by the block boundary, preserving the recognized format and not merging paragraphs (约束八:不合并段落、保持识别的格式). The installer states both flags false for this deployment.

The outline pass

The merge also collects the lines a reader might call headings, and writes them to the outline file as candidates:

FieldMeaning
idh1, h2, … in document order
pageThe page the line came from
textThe line as the document renders it
levelThe level the document used: the correction when one was applied, else the inferred level, or null for body text
inferredThe level the merge inferred, before any correction
ratioThe line's glyph height over the document's body height
numberedThe section number the line opens with, or null

A line becomes a candidate when the merge would call it a heading, when it opens with a section number, when it is taller than outlineCandidateRatio of the body height, or when it names a known section. Those last two are what put body lines in the list as well: a line the merge left alone is exactly the kind of line a reader can see is not a heading, and the model needs to be able to say so.

The model answers with levels, a compact list of h<id>=<level>. 1 to 6 makes that line a heading at that depth; 0 leaves it as body text, which is how a line the recognition promoted is demoted again. A second ocr call carrying levels reads the stored geometry, re-runs the same merge with those levels applied, and rewrites the Markdown. It does not render or recognize anything, so it costs the merge rather than the document — three pages answered in about a second where the recognition took thirty.

The corrections become part of the record, and the outline keeps both the applied level and the inferred one. Recognizing the same source again therefore reproduces the corrected document at the path it already owns rather than writing an uncorrected one beside it, and a caller re-reading the outline can see that its correction took.

Export shape

The entry module named-exports name, inject, Config, and apply and carries no default export. Config applies the documented defaults and requires every bound the deployment owns. The entry re-exports the pure core its suites drive — the request planning, the artifact digest and naming, the correction parser, the finished-document reader, the result renderer, and the line framing — and the orchestration and artifact-writing modules behind recognize, so the model-facing surface and the testable core are both reachable from the one . export.


Model Experience

System-prompt section

What the model sees

tool:ocr at order 2975, after the built-in tool band. It says when to reach for the tool, that the call saves a document and an outline file, that the heading levels can be corrected by calling again with levels, and that the document is what to pass to present. The text is empty wherever ocr is not visible in that scope.

Token effect

Three sentences, constant — 438 characters measured.

KV Cache effect

None; the section is a function of tool visibility, not of the conversation.

Tool schema

What the model sees

Five parameters: path, paths, pages, outputDir, and levels. path and paths are alternatives.

Token effect

Roughly constant — 1275 characters of JSON Schema measured on a booted row, of which the parameters are 953 and the description 277. The plugin's whole fixed per-request cost is 1713 characters against 1561 before the outline pass existed: the correction step and the present hand-off cost about ten percent more per request.

KV Cache effect

None. The schema does not vary with the conversation.

Tool-call history and result

What the model sees

The call as made, then one block per document (source path, saved path, page counts, line and heading counts, character count, what was removed, and the outline file), followed by one block per failure. A correction reports what it corrected instead.

Token effect

Proportional to the number of documents and failures, not to document size: the file path is reported instead of its contents, and the outline is a file rather than a list in the result. A three-page document costs 341 characters of result. The recognized text enters the conversation only if the model subsequently reads the Markdown it was told about.

KV Cache effect

None on its own. The result enters the transcript on the following turn like any other tool result.

Auto-invocation

The package also recognizes its trigger on its own, before the model reads the section above. A ctx.on('agent/pre-step', …) listener scans the inbound user message for the same two signals the model is told about — a mentioned PDF or image path (the worker's own SUPPORTED_EXTENSIONS set, so it never primes for a file the tool would reject) and an explicit OCR / text-extraction intent — and, when one is present and ocr is visible in the assembling scope, folds a priming instruction in front of the step so the call fires without the model first having to notice the file. The detection is the pure detectOcrTriggers in src/trigger.ts; the listener gates on ctx.tools.get(TOOL_NAME) and acts on the inbound user message alone, so it does not re-fire on a tool result or on its own prime. This is layer 2 of the skill's auto-invocation reference; the section above is layer 1.


Evidence

FileProves
tests/index.test.mjsRequest planning, page validation, the artifact layout, the correction pass, every rejection, the deployment bounds that have no default, and the heuristics that keep one
tests/transport.test.mjsA real child process: page events written to the right files, an out-of-order page aborting the call, a silent worker, the time ceiling, and one failed document leaving the rest of the batch intact
tests/filename.test.mjsThe stem rules, the digest name, and the five-digit page file
tests/render.test.mjsThe model-facing result of a recognition, a correction, a failure, and a truncated outline
tests/presentation.test.mjsThe presentation metadata, and that no result text carries deployment vocabulary
tests/load-path.test.mjsThe module form through the real Loader: no default export, inject kept
tests/loader-composition.test.mjsA real cordis.yml boots, the row's bounds are live, an omitted bound fails the load, the section reaches the assembled prompt, and a call dispatched through the registered tool drives the worker and writes every artifact through the mounted capability
tests/hmr-safety.test.mjsThe registrations leave with their contributing fiber
tests/sandbox-write.test.mjsThe artifact writes under a real workspace-write backend: the fence is required, lands the document inside the session workspace, refuses a path outside it, restates the refusal, and leaves a non-denial alone
tests/conventions.test.mjsThe source rules the skill scaffolds a package with
tests/deployment.mjsThe bounds a suite configures the tool with, so no suite inherits one from the package
tests/trigger.test.mjsThe auto-invocation detector: a mentioned image/PDF path fires, an unsupported extension does not, an explicit OCR intent fires without a path, and a path plus intent report in order without duplication
python/tests/The pure layout, cleaning, merge, page-numbering, candidate, and override rules — 89 cases, no third-party import

./invariant

The package publishes none. An invariant companion is warranted only when independent observations of one owned relation can diverge; here the merge is a pure function of the page records one process produced, and the artifact layout is derived from a digest of its own inputs. The worker's and the plugin's suites cover those relations instead.

Known Limitations and Deferred Work

  • The row must state every bound the deployment owns. There is no fallback: a profile that mounts this tool without dpi, the pixel, page, file, outline, and time ceilings, the engine lifetime, and the per-page policy fails to load. That is deliberate — a hidden default would decide how much memory and time this machine spends without anyone having agreed to it — but it does mean a copied patch row from an earlier release needs those values added.
  • A read-only session saves nothing. Every write goes through the sandboxed filesystem capability and is stamped with the calling session's per-call policy, so the call fails with the standard denial marker when the session forbids writing, and a write whose path falls outside the session workspace is refused by name.
  • A composition that mounts a confining filesystem without ctx.sandboxPolicy is refused at the first call. The tool borrows both capabilities rather than declaring them, so the row itself still mounts; the misconfiguration surfaces as one readable error on the first call instead of as artifacts refused one page at a time.
  • The outline pass is one model turn. A document whose structure the model gets wrong needs a second levels call. A correction replaces the levels it names and leaves the rest as the recognition inferred them, and the worker reports the inferred level alongside the applied one, so a correction can be revised rather than only repeated.
  • The candidate list is capped. Past maxOutlineCandidates the remaining candidates are dropped and the result says the outline is incomplete.
  • Nothing here reads a PDF's embedded text layer. Every page is rendered and recognized, which is what makes a scan work and what costs time on a text PDF.
  • Recognition quality is the engine's. Small type, dense tables, handwriting, and mathematical notation are recognized poorly or not at all.
  • Tables are not reconstructed. Cells on one row become one line, so a table is emitted as prose.
  • Column detection finds one gutter. A page with three columns, or with full-width headings between columns, is read as two.
  • A running head needs repetition to be recognized. A document shorter than runningHeadMinPages, or a page selection covering fewer pages, keeps its running head as body text.
  • The tool needs a local filesystem. Its renderer opens the source by path, so a remote filesystem backend cannot serve it.
  • The engine's first use loads its models, so expect a few seconds before the first page is recognized; moves that cost to installation.

Dev Note

Built with the plugin-development skill's scaffolder, node <checkout>/.dsh/skills/dsh-plugin-development/scripts/src/index.mjs ocr --tool --plugins-root ., which wrote the package skeleton, the profile patch row, and the profile link dependency in one run.

pnpm build        # tsc -p tsconfig.json && tsdown
pnpm test         # node --test over the built output
pnpm run setup    # create python/.venv and verify it
pnpm test:python  # the pure layout, cleaning, and merge rules

pnpm test runs against lib/, so build first. The Python suite has no third-party dependency and runs under any Python 3; it does not need the environment pnpm run setup creates.

The two halves are specified by one wire protocol, documented in python/README.md. The host validates what the worker reports rather than trusting it, and every field the host reads is emitted by a test fixture as well as by the real worker.

runningHeadRatio0.6Fraction of pages such a line must appear on
runningHeadMinPages3Absolute page minimum for that rule
engineLifetimeyes—Rebuild the engine per document, or share it across the call
timeoutMsyes—Ceiling on one recognition; 0 disables it
startupTimeoutMsyes—Ceiling on the worker's startup check before the call is refused
maxPagesyes—Pages a document may carry without an explicit selection; 0 disables it
maxDocumentsyes—Files one call may name
joinAcrossPagestrueRejoin a paragraph the page break cut in half (约束八:上下页不合并 when false)
joinWrappedLinestrueJoin a wrapped continuation line into its paragraph (约束八:段落不合并、格式保持 when false)
--prefetch
  • The environment is large — about 260 MB, of which 30 MB is the model weights — and lives inside the package directory at python/.venv.