docmd-cli 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/ARCHITECTURE.md +28 -1
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/PKG-INFO +31 -2
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/README.md +30 -1
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/__init__.py +1 -1
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/converters/marker_converter.py +26 -1
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/errors.py +24 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/postprocess/image_handling.py +11 -1
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/pyproject.toml +1 -1
- docmd_cli-0.1.2/tests/test_marker_converter_errors.py +49 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/test_postprocess.py +37 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/test_postprocess_more_integration.py +23 -9
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/.github/workflows/ci.yml +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/.github/workflows/release.yml +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/.gitignore +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/LICENSE +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/cli.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/config.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/converters/__init__.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/converters/base.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/converters/registry.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/postprocess/__init__.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/postprocess/heading_normalize.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/docmd/postprocess/table_cleanup.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/generate_fixtures.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/generate_stress_fixtures.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/merged_cells.pdf +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/running_header.pdf +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/sample.docx +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/sample.pdf +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/stress.pdf +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/fixtures/with_image.pdf +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/test_cli.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/test_converters.py +0 -0
- {docmd_cli-0.1.1 → docmd_cli-0.1.2}/tests/test_postprocess_integration.py +0 -0
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
## The pitch (keep this pinned above your desk)
|
|
4
4
|
We are not competing on conversion quality. We are selling **"hit an endpoint, get clean Markdown back"** — no Python env, no 8GB+ RAM, no GPU, no dependency hell. The open-source core proves the engine works and builds trust. The hosted API sells convenience.
|
|
5
5
|
|
|
6
|
+
## Real dependency gap found by testing (2026-09-18)
|
|
7
|
+
The pitch above says "no dependency hell." That held for text-layer PDFs, DOCX, and
|
|
8
|
+
PPTX when actually tested against real downloaded documents (an arXiv paper, an IMF
|
|
9
|
+
report). It did **not** hold for OCR: Marker's current recognition model (used for
|
|
10
|
+
both scanned-PDF OCR and equation recognition) is a VLM served through either `vllm`
|
|
11
|
+
or llama.cpp's `llama-server` binary - no plain-transformers/CPU fallback exists in
|
|
12
|
+
this version. Neither ships via pip, so `pip install docmd-cli[full]` alone cannot
|
|
13
|
+
actually OCR a scanned document or handle a PDF with equations, despite the README
|
|
14
|
+
previously implying "OCR - bundled by Marker, free" meant it worked out of the box.
|
|
15
|
+
|
|
16
|
+
Confirmed via a real test: converting a genuinely scanned PDF (downloaded from
|
|
17
|
+
archive.org) raised a missing-binary error on this dev machine (no Homebrew). Fixed
|
|
18
|
+
two ways: (1) a real fix - download a prebuilt `llama-server` binary directly from
|
|
19
|
+
llama.cpp's GitHub releases (no package manager needed) and set `LLAMA_CPP_BINARY`,
|
|
20
|
+
which then produced correct real OCR output; (2) a docmd fix - a dedicated
|
|
21
|
+
`MissingSystemDependencyError` with install instructions instead of a raw stack trace
|
|
22
|
+
when the binary is missing (see `docmd/errors.py`).
|
|
23
|
+
|
|
24
|
+
**Implications to carry into Stage 2/3 (`api/`)**: the hosted API's Docker image
|
|
25
|
+
does not currently install `llama-server` either - meaning a customer uploading a
|
|
26
|
+
scanned PDF or an equation-heavy document to the live API would hit this same error
|
|
27
|
+
today. This needs a fix in `docmd-api`'s Dockerfile before OCR can be honestly
|
|
28
|
+
advertised as working end-to-end there. Not yet fixed as of this note - flagged here
|
|
29
|
+
so it isn't lost.
|
|
30
|
+
|
|
6
31
|
## Positioning risk (checked 2026-09-18)
|
|
7
32
|
Marker's *code* is Apache-2.0 (no restriction). Marker's *model weights* use a modified
|
|
8
33
|
Open RAIL-M license: free for research, personal use, and organizations under $5M in
|
|
@@ -128,7 +153,9 @@ Treat this folder as the actual differentiator. A thin wrapper is a weekend proj
|
|
|
128
153
|
|
|
129
154
|
- `pip install docmd-cli` -> core PDF conversion (Marker bundles OCR here already; there is
|
|
130
155
|
no separate lean/no-OCR base install, since Marker's own base dependencies include
|
|
131
|
-
`surya-ocr`)
|
|
156
|
+
`surya-ocr`) - but actually *running* OCR (or equation recognition) on top of that
|
|
157
|
+
also needs the `llama-server` native binary, which no `pip install` variant can
|
|
158
|
+
provide - see "Real dependency gap found by testing" above
|
|
132
159
|
- `pip install docmd-cli[full]` -> adds DOCX/PPTX/EPUB/XLSX support via Marker's `full` extra
|
|
133
160
|
|
|
134
161
|
## License clarity
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: docmd-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Convert PDFs, DOCX, and PPTX to clean, structure-preserving Markdown.
|
|
5
5
|
Project-URL: Homepage, https://github.com/taherzribi/docmd
|
|
6
6
|
Project-URL: Issues, https://github.com/taherzribi/docmd/issues
|
|
@@ -67,7 +67,7 @@ core, usable standalone today.
|
|
|
67
67
|
| Input | Output |
|
|
68
68
|
|---|---|
|
|
69
69
|
| PDF (text-based) | Markdown with preserved headings, lists, tables |
|
|
70
|
-
| PDF (scanned) | Markdown via OCR — bundled by Marker, free |
|
|
70
|
+
| PDF (scanned) | Markdown via OCR — bundled by Marker, free, but needs [one extra native binary](#ocr-and-equations-need-one-native-binary) |
|
|
71
71
|
| DOCX | Markdown with formatting preserved (`pip install docmd-cli[full]`) |
|
|
72
72
|
| PPTX | Markdown, one section per slide (`pip install docmd-cli[full]`) |
|
|
73
73
|
|
|
@@ -116,6 +116,35 @@ sudo apt-get install libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 libf
|
|
|
116
116
|
|
|
117
117
|
PDF conversion (the base install) does not need this.
|
|
118
118
|
|
|
119
|
+
## OCR and equations need one native binary
|
|
120
|
+
|
|
121
|
+
Found by testing docmd against a real scanned document, not documented anywhere
|
|
122
|
+
upstream at the time of writing: Marker's current OCR and equation-recognition model
|
|
123
|
+
is a vision-language model served through either `vllm` (GPU/Linux-oriented) or
|
|
124
|
+
[llama.cpp](https://github.com/ggml-org/llama.cpp)'s `llama-server` binary - there is
|
|
125
|
+
no plain-CPU/transformers fallback. `pip install docmd-cli` cannot provide either one,
|
|
126
|
+
since neither ships as a normal Python wheel.
|
|
127
|
+
|
|
128
|
+
This only matters for **scanned PDFs** (no embedded text layer) and PDFs with
|
|
129
|
+
**equations** - a normal text-layer PDF never touches this code path, and everything
|
|
130
|
+
else in this README works with just `pip install`.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# macOS / Linux with Homebrew
|
|
134
|
+
brew install llama.cpp
|
|
135
|
+
|
|
136
|
+
# No Homebrew: download a prebuilt binary directly, no package manager needed
|
|
137
|
+
# (pick the archive matching your OS/arch from the releases page)
|
|
138
|
+
curl -LO https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-<version>-bin-macos-arm64.tar.gz
|
|
139
|
+
tar xzf llama-<version>-bin-macos-arm64.tar.gz
|
|
140
|
+
export LLAMA_CPP_BINARY=$PWD/llama-<version>/llama-server
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Without it, converting a scanned PDF or one with equations raises a
|
|
144
|
+
`MissingSystemDependencyError` with these same instructions - not a raw stack trace.
|
|
145
|
+
First real OCR run also downloads the model's GGUF weights from Hugging Face
|
|
146
|
+
(a few GB), separate from the PyTorch weights Marker already downloaded.
|
|
147
|
+
|
|
119
148
|
## Image handling
|
|
120
149
|
|
|
121
150
|
Images default to a text placeholder (`*[... omitted]*`) - no binary data, nothing to
|
|
@@ -40,7 +40,7 @@ core, usable standalone today.
|
|
|
40
40
|
| Input | Output |
|
|
41
41
|
|---|---|
|
|
42
42
|
| PDF (text-based) | Markdown with preserved headings, lists, tables |
|
|
43
|
-
| PDF (scanned) | Markdown via OCR — bundled by Marker, free |
|
|
43
|
+
| PDF (scanned) | Markdown via OCR — bundled by Marker, free, but needs [one extra native binary](#ocr-and-equations-need-one-native-binary) |
|
|
44
44
|
| DOCX | Markdown with formatting preserved (`pip install docmd-cli[full]`) |
|
|
45
45
|
| PPTX | Markdown, one section per slide (`pip install docmd-cli[full]`) |
|
|
46
46
|
|
|
@@ -89,6 +89,35 @@ sudo apt-get install libpango-1.0-0 libpangocairo-1.0-0 libgdk-pixbuf-2.0-0 libf
|
|
|
89
89
|
|
|
90
90
|
PDF conversion (the base install) does not need this.
|
|
91
91
|
|
|
92
|
+
## OCR and equations need one native binary
|
|
93
|
+
|
|
94
|
+
Found by testing docmd against a real scanned document, not documented anywhere
|
|
95
|
+
upstream at the time of writing: Marker's current OCR and equation-recognition model
|
|
96
|
+
is a vision-language model served through either `vllm` (GPU/Linux-oriented) or
|
|
97
|
+
[llama.cpp](https://github.com/ggml-org/llama.cpp)'s `llama-server` binary - there is
|
|
98
|
+
no plain-CPU/transformers fallback. `pip install docmd-cli` cannot provide either one,
|
|
99
|
+
since neither ships as a normal Python wheel.
|
|
100
|
+
|
|
101
|
+
This only matters for **scanned PDFs** (no embedded text layer) and PDFs with
|
|
102
|
+
**equations** - a normal text-layer PDF never touches this code path, and everything
|
|
103
|
+
else in this README works with just `pip install`.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# macOS / Linux with Homebrew
|
|
107
|
+
brew install llama.cpp
|
|
108
|
+
|
|
109
|
+
# No Homebrew: download a prebuilt binary directly, no package manager needed
|
|
110
|
+
# (pick the archive matching your OS/arch from the releases page)
|
|
111
|
+
curl -LO https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-<version>-bin-macos-arm64.tar.gz
|
|
112
|
+
tar xzf llama-<version>-bin-macos-arm64.tar.gz
|
|
113
|
+
export LLAMA_CPP_BINARY=$PWD/llama-<version>/llama-server
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Without it, converting a scanned PDF or one with equations raises a
|
|
117
|
+
`MissingSystemDependencyError` with these same instructions - not a raw stack trace.
|
|
118
|
+
First real OCR run also downloads the model's GGUF weights from Hugging Face
|
|
119
|
+
(a few GB), separate from the PyTorch weights Marker already downloaded.
|
|
120
|
+
|
|
92
121
|
## Image handling
|
|
93
122
|
|
|
94
123
|
Images default to a text placeholder (`*[... omitted]*`) - no binary data, nothing to
|
|
@@ -18,7 +18,7 @@ from docmd.postprocess.image_handling import apply_image_handling
|
|
|
18
18
|
from docmd.postprocess.table_cleanup import clean_tables
|
|
19
19
|
|
|
20
20
|
__all__ = ["convert", "convert_document", "ConvertConfig", "ConversionResult"]
|
|
21
|
-
__version__ = "0.1.
|
|
21
|
+
__version__ = "0.1.2"
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def convert_document(
|
|
@@ -13,7 +13,12 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from docmd.config import ConvertConfig
|
|
15
15
|
from docmd.converters.base import ConversionResult
|
|
16
|
-
from docmd.errors import
|
|
16
|
+
from docmd.errors import (
|
|
17
|
+
ConversionError,
|
|
18
|
+
EncryptedDocumentError,
|
|
19
|
+
MissingExtraError,
|
|
20
|
+
MissingSystemDependencyError,
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
# Marker loads its models (a few hundred MB to ~1GB of weights, downloaded
|
|
19
24
|
# from Hugging Face on first use) lazily and caches them at module scope, so
|
|
@@ -21,6 +26,11 @@ from docmd.errors import ConversionError, EncryptedDocumentError, MissingExtraEr
|
|
|
21
26
|
_model_dict: dict[str, Any] | None = None
|
|
22
27
|
|
|
23
28
|
_PASSWORD_HINTS = ("password", "encrypt")
|
|
29
|
+
# Raised as surya.inference.backends.spawn.SpawnError when OCR/equation
|
|
30
|
+
# recognition needs the llama-server binary and it isn't on PATH - matched
|
|
31
|
+
# by message/type name rather than importing surya's internal exception
|
|
32
|
+
# class, so this doesn't break if that module path moves.
|
|
33
|
+
_MISSING_BINARY_HINTS = ("llama-server", "spawnerror")
|
|
24
34
|
|
|
25
35
|
|
|
26
36
|
def _get_model_dict() -> dict[str, Any]:
|
|
@@ -39,6 +49,18 @@ def _build_config_dict(config: ConvertConfig):
|
|
|
39
49
|
"output_format": "markdown",
|
|
40
50
|
"force_ocr": config.force_ocr,
|
|
41
51
|
"use_llm": config.use_llm,
|
|
52
|
+
# Marker's own default (4) spins up a multiprocessing.ProcessPoolExecutor
|
|
53
|
+
# for page-text extraction on any sufficiently multi-page PDF. On
|
|
54
|
+
# spawn-based platforms (macOS, Windows) that crashes with
|
|
55
|
+
# "An attempt has been made to start a new process before the
|
|
56
|
+
# current process has finished its bootstrapping phase" whenever the
|
|
57
|
+
# caller isn't wrapped in `if __name__ == "__main__":` - an easy trap
|
|
58
|
+
# for a library used from a plain script, a notebook, or a web
|
|
59
|
+
# server's request handler. Marker's own bundled server.py sets this
|
|
60
|
+
# to 1 for exactly this reason; docmd does the same as a library
|
|
61
|
+
# default, trading a bit of extraction parallelism for not crashing
|
|
62
|
+
# on arbitrary callers.
|
|
63
|
+
"pdftext_workers": 1,
|
|
42
64
|
}
|
|
43
65
|
if config.image_mode == "skip":
|
|
44
66
|
options["disable_image_extraction"] = True
|
|
@@ -71,8 +93,11 @@ class MarkerConverter:
|
|
|
71
93
|
rendered = converter(filepath)
|
|
72
94
|
except Exception as exc:
|
|
73
95
|
message = str(exc).lower()
|
|
96
|
+
exc_type_name = type(exc).__name__.lower()
|
|
74
97
|
if any(hint in message for hint in _PASSWORD_HINTS):
|
|
75
98
|
raise EncryptedDocumentError() from exc
|
|
99
|
+
if any(hint in message or hint in exc_type_name for hint in _MISSING_BINARY_HINTS):
|
|
100
|
+
raise MissingSystemDependencyError(str(exc)) from exc
|
|
76
101
|
raise ConversionError(
|
|
77
102
|
f"Marker failed to convert '{filepath}': {exc}", cause=exc
|
|
78
103
|
) from exc
|
|
@@ -35,6 +35,30 @@ class MissingExtraError(DocmdError):
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
class MissingSystemDependencyError(DocmdError):
|
|
39
|
+
"""Raised when Marker's OCR/equation recognition needs a native binary
|
|
40
|
+
that isn't installed - not a pip package, so `pip install docmd[full]`
|
|
41
|
+
can't provide it. See ARCHITECTURE.md "OCR and equation recognition need
|
|
42
|
+
a native binary" for why this exists."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, detail: str) -> None:
|
|
45
|
+
super().__init__(
|
|
46
|
+
"OCR/equation recognition needs the `llama-server` binary from "
|
|
47
|
+
"llama.cpp, which pip cannot install. This only matters for "
|
|
48
|
+
"scanned PDFs or documents with equations - a normal text-layer "
|
|
49
|
+
"PDF doesn't hit this path.\n\n"
|
|
50
|
+
"Install it with:\n"
|
|
51
|
+
" macOS: brew install llama.cpp\n"
|
|
52
|
+
" Linux: brew install llama.cpp, or download a release from\n"
|
|
53
|
+
" https://github.com/ggml-org/llama.cpp/releases\n"
|
|
54
|
+
"No Homebrew? Download the right archive from that releases "
|
|
55
|
+
"page directly (llama-<version>-bin-macos-arm64.tar.gz etc.), "
|
|
56
|
+
"then either put `llama-server` on your PATH or set "
|
|
57
|
+
"LLAMA_CPP_BINARY to its full path.\n\n"
|
|
58
|
+
f"Underlying error: {detail}"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
38
62
|
class EncryptedDocumentError(DocmdError):
|
|
39
63
|
"""Raised when the input is a password-protected / encrypted document."""
|
|
40
64
|
|
|
@@ -18,7 +18,7 @@ import re
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
|
-
_IMAGE_RE = re.compile(r"!\[([^\]]*)\]\(([^)]
|
|
21
|
+
_IMAGE_RE = re.compile(r"!\[([^\]]*)\]\(([^)]*)\)")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def apply_image_handling(
|
|
@@ -32,6 +32,16 @@ def apply_image_handling(
|
|
|
32
32
|
def _replace(match: re.Match[str]) -> str:
|
|
33
33
|
nonlocal counter
|
|
34
34
|
alt_text, ref = match.group(1), match.group(2)
|
|
35
|
+
|
|
36
|
+
if not ref.strip():
|
|
37
|
+
# `![]()` with no reference at all - real Marker output on some
|
|
38
|
+
# scanned pages (an OCR'd region with no recoverable image data).
|
|
39
|
+
# No mode should keep this: a placeholder/skip has nothing to
|
|
40
|
+
# describe, and alt-text mode would otherwise emit a link to
|
|
41
|
+
# nowhere (an empty href renders as a broken image for no
|
|
42
|
+
# legitimate reason, since there was never a file to begin with).
|
|
43
|
+
return ""
|
|
44
|
+
|
|
35
45
|
filename = Path(ref).name
|
|
36
46
|
|
|
37
47
|
if mode == "skip":
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Unit tests for MarkerConverter's exception classification - mocked so
|
|
2
|
+
these run fast and don't depend on whether llama-server happens to be
|
|
3
|
+
installed on the machine running the tests. See
|
|
4
|
+
docmd/converters/marker_converter.py for the real (slow, environment-
|
|
5
|
+
dependent) version of this failure, found via real OCR testing on a
|
|
6
|
+
genuinely scanned PDF from archive.org.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import docmd.converters.marker_converter as marker_converter_module
|
|
12
|
+
from docmd.config import ConvertConfig
|
|
13
|
+
from docmd.converters.marker_converter import MarkerConverter
|
|
14
|
+
from docmd.errors import MissingSystemDependencyError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _FakeSpawnError(Exception):
|
|
18
|
+
"""Stands in for surya.inference.backends.spawn.SpawnError without
|
|
19
|
+
importing surya's internal exception class directly."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _FakePdfConverterMissingBinary:
|
|
23
|
+
def __init__(self, **_kwargs) -> None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def __call__(self, _filepath: str):
|
|
27
|
+
raise _FakeSpawnError(
|
|
28
|
+
"llama-server binary not found. Install with:\n"
|
|
29
|
+
" macOS: brew install llama.cpp"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_missing_llama_server_binary_raises_clear_error(monkeypatch, tmp_path):
|
|
34
|
+
monkeypatch.setattr(marker_converter_module, "_get_model_dict", lambda: {})
|
|
35
|
+
monkeypatch.setattr(
|
|
36
|
+
"marker.converters.pdf.PdfConverter", _FakePdfConverterMissingBinary
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
dummy_pdf = tmp_path / "dummy.pdf"
|
|
40
|
+
dummy_pdf.write_bytes(b"%PDF-1.4\n%%EOF")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
MarkerConverter().convert(str(dummy_pdf), ConvertConfig())
|
|
44
|
+
raise AssertionError("expected MissingSystemDependencyError")
|
|
45
|
+
except MissingSystemDependencyError as exc:
|
|
46
|
+
message = str(exc)
|
|
47
|
+
assert "llama.cpp" in message
|
|
48
|
+
assert "brew install llama.cpp" in message
|
|
49
|
+
assert "LLAMA_CPP_BINARY" in message
|
|
@@ -9,6 +9,30 @@ def test_heading_normalize_fixes_level_skip():
|
|
|
9
9
|
assert out == "# Title\n## Subsection\n"
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
def test_heading_normalize_fixes_level_skip_then_leaves_recovered_depth_alone():
|
|
13
|
+
"""The exact pattern found in a real 27-page IMF report while stress-
|
|
14
|
+
testing against downloaded (not synthetic) documents: an H1 jumps
|
|
15
|
+
straight to H4 (skipping H2/H3), gets clamped to H2 - but a *later*
|
|
16
|
+
heading that's also originally H4 is left alone, because an H3 heading
|
|
17
|
+
in between legitimately re-establishes the depth. Never reproduced
|
|
18
|
+
synthetically (see skip_headings.pdf in git history, removed after
|
|
19
|
+
Marker's own layout model wouldn't cooperate) - this encodes the real
|
|
20
|
+
structure without redistributing the copyrighted source PDF."""
|
|
21
|
+
md = (
|
|
22
|
+
"# Summary of the Economy Classification\n"
|
|
23
|
+
"#### General Features and Composition of Groups\n"
|
|
24
|
+
"### Advanced Economies\n"
|
|
25
|
+
"#### Emerging Market and Developing Economies\n"
|
|
26
|
+
)
|
|
27
|
+
out = normalize_headings(md)
|
|
28
|
+
assert out == (
|
|
29
|
+
"# Summary of the Economy Classification\n"
|
|
30
|
+
"## General Features and Composition of Groups\n"
|
|
31
|
+
"### Advanced Economies\n"
|
|
32
|
+
"#### Emerging Market and Developing Economies\n"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
12
36
|
def test_heading_normalize_drops_empty_heading():
|
|
13
37
|
md = "# Title\n##\nBody text.\n"
|
|
14
38
|
out = normalize_headings(md)
|
|
@@ -98,3 +122,16 @@ def test_image_handling_alt_text_mode_fills_empty_alt():
|
|
|
98
122
|
md = ""
|
|
99
123
|
out = apply_image_handling(md, images={}, mode="alt-text")
|
|
100
124
|
assert out == ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_image_handling_drops_fully_empty_image_reference():
|
|
128
|
+
"""Real Marker output on a scanned page: `![]()` with no href at all,
|
|
129
|
+
directly adjacent to a real image reference with no separator between
|
|
130
|
+
them - found while OCR-testing a genuinely scanned PDF. The old regex
|
|
131
|
+
required at least one character inside the parens, so `![]()` never
|
|
132
|
+
matched and leaked through every mode unprocessed."""
|
|
133
|
+
md = "![]()"
|
|
134
|
+
|
|
135
|
+
assert apply_image_handling(md, images={}, mode="placeholder") == "*[image omitted]*"
|
|
136
|
+
assert apply_image_handling(md, images={}, mode="skip") == ""
|
|
137
|
+
assert apply_image_handling(md, images={}, mode="alt-text") == ""
|
|
@@ -16,18 +16,32 @@ FIXTURES = Path(__file__).parent / "fixtures"
|
|
|
16
16
|
|
|
17
17
|
def test_inconsistent_heading_levels_get_normalized_to_match():
|
|
18
18
|
"""running_header.pdf has 4 structurally identical section headings
|
|
19
|
-
(same paragraph style).
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
(same paragraph style). On the machine this was first written on (macOS,
|
|
20
|
+
MPS), raw Marker assigned them *different* levels - Section 1 came back
|
|
21
|
+
H3, Sections 2-4 came back H2 - a genuine model inconsistency, not a
|
|
22
|
+
deliberately constructed skip.
|
|
23
|
+
|
|
24
|
+
That specific raw output isn't portable, though: CI (Linux, CPU) ran the
|
|
25
|
+
exact same PDF through the exact same pinned Marker version and got all
|
|
26
|
+
four sections back as H2 from the start - a real platform-dependent
|
|
27
|
+
difference in the underlying ML model's inference, not something our
|
|
28
|
+
code controls. Hard-pinning the raw "bug" broke CI twice for that
|
|
29
|
+
reason. So this only *asserts* the thing docmd actually guarantees -
|
|
30
|
+
consistent heading levels across all four sections, however Marker
|
|
31
|
+
happened to render them - and reports (without failing) whether this
|
|
32
|
+
particular run's raw output shows the original inconsistency."""
|
|
23
33
|
raw = MarkerConverter().convert(str(FIXTURES / "running_header.pdf"), ConvertConfig())
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
if "### **Section 1: Findings**" in raw.markdown:
|
|
35
|
+
print("raw Marker output reproduced the H3-vs-H2 inconsistency on this run")
|
|
36
|
+
else:
|
|
37
|
+
print("raw Marker output did not reproduce the inconsistency on this run/platform")
|
|
26
38
|
|
|
27
39
|
result = convert_document(str(FIXTURES / "running_header.pdf"))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
levels = {
|
|
41
|
+
n: "###" if f"### **Section {n}: Findings**" in result.markdown else "##"
|
|
42
|
+
for n in range(1, 5)
|
|
43
|
+
}
|
|
44
|
+
assert len(set(levels.values())) == 1, f"inconsistent levels in docmd output: {levels}"
|
|
31
45
|
|
|
32
46
|
|
|
33
47
|
def test_running_header_does_not_leak_into_output():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|