any2md 1.0.0__tar.gz → 1.0.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.
- {any2md-1.0.0 → any2md-1.0.2}/.gitignore +1 -0
- {any2md-1.0.0 → any2md-1.0.2}/CHANGELOG.md +83 -0
- {any2md-1.0.0 → any2md-1.0.2}/PKG-INFO +72 -24
- {any2md-1.0.0 → any2md-1.0.2}/README.md +71 -23
- {any2md-1.0.0 → any2md-1.0.2}/any2md/__init__.py +1 -1
- {any2md-1.0.0 → any2md-1.0.2}/any2md/cli.py +27 -2
- {any2md-1.0.0 → any2md-1.0.2}/any2md/converters/docx.py +36 -2
- {any2md-1.0.0 → any2md-1.0.2}/any2md/converters/pdf.py +30 -3
- {any2md-1.0.0 → any2md-1.0.2}/any2md/frontmatter.py +33 -3
- any2md-1.0.2/any2md/heuristics.py +580 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/pipeline/__init__.py +13 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/pipeline/cleanup.py +22 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/pipeline/text.py +187 -0
- any2md-1.0.2/assets/logo-original.jpeg +0 -0
- any2md-1.0.2/assets/logo.png +0 -0
- any2md-1.0.2/assets/social-preview.png +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/docs/architecture.md +174 -59
- {any2md-1.0.0 → any2md-1.0.2}/docs/cli-reference.md +76 -1
- {any2md-1.0.0 → any2md-1.0.2}/docs/output-format.md +45 -2
- {any2md-1.0.0 → any2md-1.0.2}/docs/superpowers/plans/2026-04-26-any2md-v1-phase1-foundation.md +10 -2
- any2md-1.0.2/docs/superpowers/plans/2026-04-26-any2md-v1-phase2-docling-backend.md +1426 -0
- any2md-1.0.2/docs/superpowers/plans/2026-04-26-any2md-v1-phase3-figures-ocr-text-lane.md +1053 -0
- any2md-1.0.2/docs/superpowers/plans/2026-04-26-any2md-v1-phase4-polish-config-docs.md +936 -0
- any2md-1.0.2/docs/superpowers/plans/2026-04-26-any2md-v1.0.2-implementation.md +307 -0
- any2md-1.0.2/docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md +694 -0
- {any2md-1.0.0 → any2md-1.0.2}/docs/troubleshooting.md +11 -0
- {any2md-1.0.0 → any2md-1.0.2}/docs/upgrading-from-0.7.md +1 -0
- any2md-1.0.2/tests/cli/test_cli_backend.py +45 -0
- any2md-1.0.2/tests/cli/test_cli_no_arxiv_lookup.py +41 -0
- any2md-1.0.2/tests/integration/test_docx_backend_override.py +57 -0
- any2md-1.0.2/tests/integration/test_docx_mammoth_fallback.py +174 -0
- any2md-1.0.2/tests/integration/test_pdf_backend_override.py +57 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/integration/test_pdf_docling.py +3 -0
- any2md-1.0.2/tests/integration/test_pdf_pymupdf_fallback.py +79 -0
- any2md-1.0.2/tests/unit/pipeline/test_cleanup_html_entities.py +51 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_runner.py +18 -0
- any2md-1.0.2/tests/unit/pipeline/test_text_dedupe_toc_table.py +106 -0
- any2md-1.0.2/tests/unit/pipeline/test_text_strip_cover_artifacts.py +79 -0
- any2md-1.0.2/tests/unit/pipeline/test_text_strip_repeated_byline.py +57 -0
- any2md-1.0.2/tests/unit/pipeline/test_text_strip_web_fragments.py +63 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_frontmatter_compose.py +69 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_frontmatter_helpers.py +1 -1
- any2md-1.0.2/tests/unit/test_heuristics.py +507 -0
- any2md-1.0.0/tests/integration/test_docx_mammoth_fallback.py +0 -42
- any2md-1.0.0/tests/integration/test_pdf_pymupdf_fallback.py +0 -37
- {any2md-1.0.0 → any2md-1.0.2}/CONTRIBUTING.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/LICENSE +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/__main__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/_docling.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/config.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/converters/__init__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/converters/html.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/converters/txt.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/pipeline/structured.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/utils.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/any2md/validators.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/docs/superpowers/specs/2026-04-26-any2md-v1-design.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/pyproject.toml +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/requirements.txt +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/scripts/validate-phase1.sh +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/__init__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/__init__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/test_cli_existing_flags.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/test_cli_exit_codes.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/test_cli_high_fidelity.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/test_cli_meta.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/test_cli_ocr_save_images.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/test_cli_quiet_verbose.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/cli/test_cli_strict.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/conftest.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/__init__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/docs/ligatures_and_softhyphens.txt +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/docs/multi_column.pdf +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/docs/table_heavy.docx +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/docs/web_page.html +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/make_fixtures.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/snapshots/ligatures_and_softhyphens.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/snapshots/multi_column.docling.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/snapshots/multi_column.fallback.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/snapshots/table_heavy.docling.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/snapshots/table_heavy.fallback.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/fixtures/snapshots/web_page.md +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/integration/__init__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/integration/test_docx_docling.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/integration/test_html_trafilatura.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/integration/test_save_images.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/integration/test_snapshots.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/integration/test_txt.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/__init__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/__init__.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_cleanup_footnote_markers.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_cleanup_ligatures.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_cleanup_nfc.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_cleanup_quotes_dashes.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_cleanup_soft_hyphens.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_cleanup_validate.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_cleanup_whitespace.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_structured_compact_tables.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_structured_heading_hierarchy.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_structured_lift_figures.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_structured_normalize_cites.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_text_dedupe_paragraphs.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_text_dedupe_toc.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_text_dehyphenate.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_text_repair_line_wraps.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_text_restore_lists_code.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/pipeline/test_text_strip_headers_footers.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_auto_id.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_config.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_content_hash.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_docling_helper.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_meta_overrides.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_pdf_complexity.py +0 -0
- {any2md-1.0.0 → any2md-1.0.2}/tests/unit/test_validators.py +0 -0
|
@@ -4,6 +4,89 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
+
## [1.0.2] — 2026-04-26
|
|
8
|
+
|
|
9
|
+
Patch release. Closes issue #15 plus 8 additional quality issues
|
|
10
|
+
discovered during deep investigation against four real-world inputs:
|
|
11
|
+
arxiv academic paper, ISO/IEC 27002 standard, COMP4441 academic
|
|
12
|
+
DOCX, and a Wikipedia article via URL.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- New `any2md/heuristics.py` module — pure functions for frontmatter
|
|
16
|
+
field refinement: `refine_title`, `refine_abstract`, `extract_authors`,
|
|
17
|
+
`filter_organization`, `arxiv_lookup`, `is_arxiv_filename`. Called
|
|
18
|
+
from `frontmatter.compose()` and from converter modules.
|
|
19
|
+
- New `produced_by` extension field on `SourceMeta` and frontmatter
|
|
20
|
+
(between `extracted_via` and `pages`). Records the software that
|
|
21
|
+
produced the source file (PDF `Creator`, DOCX `Application`).
|
|
22
|
+
Distinct from `extracted_via` which records the any2md backend
|
|
23
|
+
that produced the markdown.
|
|
24
|
+
- New shared cleanup stage **C8 `decode_html_entities`** — universal
|
|
25
|
+
removal of `&`, `>`, `<`, numeric entities (`—`,
|
|
26
|
+
`—`) from body. Code-block aware (skips fenced ` ``` ` blocks).
|
|
27
|
+
- New text-lane stages T7-T10 (aggressive/maximum profile only):
|
|
28
|
+
- **T7 `dedupe_toc_table`** — strips table-formatted TOCs that T4's
|
|
29
|
+
text-formatted-TOC heuristic doesn't catch. Common in academic PDFs.
|
|
30
|
+
- **T8 `strip_cover_artifacts`** — drops cover-page noise (QR-code
|
|
31
|
+
blurbs, version stamps like "Third edition 2022-02") in the first
|
|
32
|
+
~30 lines, before the first H2.
|
|
33
|
+
- **T9 `strip_repeated_byline`** — removes "Author's Contact
|
|
34
|
+
Information:" and similar lines that duplicate a byline.
|
|
35
|
+
- **T10 `strip_web_fragments`** — drops trafilatura extraction
|
|
36
|
+
fragments (orphan `|` / `>` lines, short incomplete sentences
|
|
37
|
+
surrounded by blank lines).
|
|
38
|
+
- New CLI flag `--no-arxiv-lookup` to disable the arxiv API metadata
|
|
39
|
+
enrichment. Arxiv enrichment is on by default for filenames matching
|
|
40
|
+
`\d{4}\.\d{4,5}` (e.g., `2501.17755v1.pdf`); SSRF-guarded with 5s
|
|
41
|
+
timeout; failures emit non-blocking warnings.
|
|
42
|
+
- Project logo and brand assets under `assets/`.
|
|
43
|
+
|
|
44
|
+
### Fixes
|
|
45
|
+
The eleven quality issues — see issue #15 and
|
|
46
|
+
`docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md`:
|
|
47
|
+
- A1: authors not extracted from PDF body byline (academic PDFs).
|
|
48
|
+
- A2: abstract picked the byline / cover blurb / TOC line.
|
|
49
|
+
- A3: organization populated with PDF Creator software junk.
|
|
50
|
+
- B1: HTML entities leaked to body universally.
|
|
51
|
+
- C1: ISO/TR titles detected as "INTERNATIONAL STANDARD" cover header.
|
|
52
|
+
- C3: DOCX titles concatenated course code + project title.
|
|
53
|
+
- C4: Wikipedia titles kept "Wikipedia:" namespace prefix.
|
|
54
|
+
- D1: TOCs dumped as markdown tables in academic PDFs.
|
|
55
|
+
- D2: trafilatura fragments leaked into web outputs.
|
|
56
|
+
- D3: ISO cover-page QR-code blurb leaked into body.
|
|
57
|
+
- E1: "Author's Contact Information:" line duplicated byline.
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- `frontmatter.compose()` now consults `heuristics.py` to refine
|
|
61
|
+
title / abstract / authors before YAML emission.
|
|
62
|
+
- `--profile conservative` now also gates the new heuristic
|
|
63
|
+
aggressiveness (skip-lists active; speculative inferences off).
|
|
64
|
+
- PDF `Creator` software values (LaTeX, acmart, Adobe InDesign,
|
|
65
|
+
Microsoft Word, etc.) no longer populate `organization`. They
|
|
66
|
+
go to the new `produced_by` field instead.
|
|
67
|
+
|
|
68
|
+
## [1.0.1] — 2026-04-26
|
|
69
|
+
|
|
70
|
+
Patch release. Adds an explicit backend-selection CLI flag.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
- `--backend {docling,pymupdf4llm,mammoth}` CLI flag and corresponding
|
|
74
|
+
`PipelineOptions.backend` field. Lets users override the automatic
|
|
75
|
+
backend selection. Useful when Docling extracts incorrectly on a
|
|
76
|
+
specific input and the user wants to fall back to pymupdf4llm
|
|
77
|
+
without uninstalling Docling. Mismatched format/backend combinations
|
|
78
|
+
(e.g., `--backend pymupdf4llm` on a DOCX) error out per file.
|
|
79
|
+
|
|
80
|
+
### Fixes (workaround)
|
|
81
|
+
- Refs #13 — Docling drops list items on certain academic PDFs with
|
|
82
|
+
the "Parent element of the list item is not a ListGroup" warning.
|
|
83
|
+
No upstream fix yet; users hitting this can now `any2md --backend
|
|
84
|
+
pymupdf4llm doc.pdf` to use the lightweight backend instead.
|
|
85
|
+
|
|
86
|
+
### Note
|
|
87
|
+
- `--backend docling` is equivalent to the existing `--high-fidelity`
|
|
88
|
+
/ `-H` flag (both kept). Future releases may consolidate.
|
|
89
|
+
|
|
7
90
|
## [1.0.0] — 2026-04-26
|
|
8
91
|
|
|
9
92
|
First stable release of any2md v1.0. Validated end-to-end against
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: any2md
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Convert PDF, DOCX, HTML, and TXT files — or web pages by URL — to clean, LLM-optimized Markdown with YAML frontmatter.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rocklambros/any2md
|
|
6
6
|
Project-URL: Issues, https://github.com/rocklambros/any2md/issues
|
|
@@ -28,10 +28,30 @@ Provides-Extra: high-fidelity
|
|
|
28
28
|
Requires-Dist: docling>=2.0.0; extra == 'high-fidelity'
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="assets/logo.png" alt="any2md" width="500"/>
|
|
33
|
+
</p>
|
|
34
|
+
|
|
31
35
|
# any2md
|
|
32
36
|
|
|
33
37
|
Convert PDFs, DOCX, HTML, URLs, and plain text into structured, machine-consumable Markdown for downstream RAG pipelines.
|
|
34
38
|
|
|
39
|
+
## What's new in 1.0.2
|
|
40
|
+
|
|
41
|
+
A patch release focused on frontmatter and body-content quality, validated against four representative inputs (an arxiv academic PDF, an ISO/IEC standard, an academic DOCX, a Wikipedia URL). Eleven distinct artifacts are addressed:
|
|
42
|
+
|
|
43
|
+
- New `any2md/heuristics.py` module — pure-functions library that `frontmatter.compose()` consults to refine `title`, `abstract_for_rag`, `authors`, and `organization` before emission.
|
|
44
|
+
- New `produced_by` extension field, sitting between `extracted_via` and `pages`. Records the software that produced the source file (PDF `Creator`, DOCX `Application`). Distinct from `extracted_via`, which records the any2md backend.
|
|
45
|
+
- New shared cleanup stage **C8 `decode_html_entities`** — universal removal of `&`, `<`, `>`, and numeric entities (`—`, `—`) from body text. Code-block aware.
|
|
46
|
+
- Four new text-lane stages (aggressive/maximum profile only):
|
|
47
|
+
- **T7 `dedupe_toc_table`** — strips table-formatted TOCs that the existing T4 text-formatted heuristic doesn't catch. Common on academic PDFs.
|
|
48
|
+
- **T8 `strip_cover_artifacts`** — drops cover-page noise (QR-code blurbs, "Third edition 2022-02"-style version stamps) that appears before the first H2.
|
|
49
|
+
- **T9 `strip_repeated_byline`** — removes "Author's Contact Information:" and similar lines that duplicate a byline.
|
|
50
|
+
- **T10 `strip_web_fragments`** — drops trafilatura extraction fragments (orphan `|` and `>` lines, short incomplete sentences surrounded by blank lines).
|
|
51
|
+
- New CLI flag `--no-arxiv-lookup`. Arxiv enrichment for filenames matching `\d{4}\.\d{4,5}` is **on by default**; this flag disables it for airgapped environments.
|
|
52
|
+
|
|
53
|
+
The full design rationale lives in [docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md](docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md). Per-fix detail is in the [CHANGELOG](CHANGELOG.md).
|
|
54
|
+
|
|
35
55
|
## What this is
|
|
36
56
|
|
|
37
57
|
any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SSRM-compatible (Structured Security Reasoning Markdown — a documented schema for LLM-consumable documents), so retrieval pipelines, embeddings jobs, and chunking utilities downstream can rely on field names, types, and a deterministic `content_hash` for cache invalidation. The body is NFC-normalized with LF line endings, and the heading hierarchy is guaranteed to start at H1 and not skip levels. The goal is one stable shape, regardless of whether a document started life as a scanned PDF, a Word doc, a Wikipedia page, or a plain text dump.
|
|
@@ -144,6 +164,7 @@ source_file: "safety-alignment.docx" # original filename (any2md e
|
|
|
144
164
|
type: "docx" # source format (any2md extension field)
|
|
145
165
|
word_count: 14289 # any2md extension field
|
|
146
166
|
extracted_via: "docling" # which backend produced the markdown
|
|
167
|
+
produced_by: "Microsoft® Word for Microsoft 365" # software that produced the source (new in v1.0.2)
|
|
147
168
|
---
|
|
148
169
|
```
|
|
149
170
|
|
|
@@ -226,7 +247,7 @@ You'd run this when your corpus has multi-column PDFs, complex tables, or scanne
|
|
|
226
247
|
pip install "any2md[high-fidelity]"
|
|
227
248
|
```
|
|
228
249
|
|
|
229
|
-
After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`.
|
|
250
|
+
After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`. Use `--backend pymupdf4llm` to force the lightweight backend even when Docling is installed (useful when Docling produces extraction artifacts on a specific input).
|
|
230
251
|
|
|
231
252
|
### When do I need high-fidelity?
|
|
232
253
|
|
|
@@ -408,6 +429,10 @@ type_code = "GD"
|
|
|
408
429
|
|
|
409
430
|
With this file in place and `--auto-id` passed, generated `document_id` values look like `CSA-2026-GD-a3f1c91d`. The resolution order, highest priority first: explicit `--meta KEY=VAL` arguments, then `--meta-file PATH` (when given), then auto-discovered `.any2md.toml`, then the tool's defaults. Full flag and config reference in [docs/cli-reference.md](docs/cli-reference.md).
|
|
410
431
|
|
|
432
|
+
### Network behavior
|
|
433
|
+
|
|
434
|
+
For PDFs whose filename matches the arxiv ID pattern (`\d{4}\.\d{4,5}`, e.g. `2501.17755v1.pdf`), any2md by default queries `https://export.arxiv.org/api/query` to enrich `authors`, `abstract_for_rag`, and `date` from the official arxiv metadata. The lookup is SSRF-guarded, has a 5-second timeout, and emits a non-blocking warning on any failure — conversion proceeds either way. Pass `--no-arxiv-lookup` to disable this for airgapped environments or when you want any2md to make no outbound calls. See [docs/cli-reference.md](docs/cli-reference.md#--no-arxiv-lookup) for details.
|
|
435
|
+
|
|
411
436
|
### Output verbosity and exit codes
|
|
412
437
|
|
|
413
438
|
The CLI has three verbosity flags that pair with a four-value exit-code contract:
|
|
@@ -442,7 +467,7 @@ The full symptom-cause-fix triage guide, plus deeper diagnosis steps and follow-
|
|
|
442
467
|
any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy backends (Docling), one for backends that need heavier text repair (pymupdf4llm, trafilatura, mammoth, the TXT heuristic). Both lanes converge on a shared cleanup pass, and the same `frontmatter.py` module emits YAML for every input format.
|
|
443
468
|
|
|
444
469
|
```
|
|
445
|
-
any2md v1.0
|
|
470
|
+
any2md v1.0.2
|
|
446
471
|
│
|
|
447
472
|
┌────┴────┐
|
|
448
473
|
CLI │ cli.py │ parse args, classify URL/file/dir,
|
|
@@ -459,43 +484,66 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
459
484
|
│ pymupdf4llm│ mammoth+md │ │
|
|
460
485
|
└────┬───────┴────┬───────┴───┬───┘
|
|
461
486
|
│ │ │
|
|
487
|
+
│ SourceMeta carries:
|
|
488
|
+
│ organization, produced_by ◀ NEW in v1.0.2
|
|
489
|
+
│ (split via heuristics.filter_organization)
|
|
490
|
+
│
|
|
462
491
|
structured-md structured-md text-md
|
|
463
492
|
│ │ │
|
|
464
493
|
└─────┬──────┴───────────┘
|
|
465
494
|
▼
|
|
466
495
|
┌─────────────────────────┐
|
|
467
496
|
│ pipeline/structured.py │ structured lane
|
|
468
|
-
│
|
|
469
|
-
│
|
|
470
|
-
│
|
|
471
|
-
│
|
|
497
|
+
│ S1 figure caption pass │ (trusts layout)
|
|
498
|
+
│ S2 table compactor │
|
|
499
|
+
│ S3 cite normalizer │
|
|
500
|
+
│ S4 heading hierarchy │
|
|
472
501
|
└────────────┬────────────┘
|
|
473
502
|
│
|
|
474
503
|
┌────────────┴────────────┐
|
|
475
504
|
│ pipeline/text.py │ text lane
|
|
476
|
-
│
|
|
477
|
-
│
|
|
478
|
-
│
|
|
479
|
-
│
|
|
480
|
-
│
|
|
481
|
-
│
|
|
505
|
+
│ T1 line-wrap repair │
|
|
506
|
+
│ T2 dehyphenate │
|
|
507
|
+
│ T9 strip repeated │ ◀ NEW (E1)
|
|
508
|
+
│ byline │
|
|
509
|
+
│ T3 dedupe paragraphs │
|
|
510
|
+
│ T4 dedupe TOC block │
|
|
511
|
+
│ T7 dedupe TOC table │ ◀ NEW (D1)
|
|
512
|
+
│ T5 hdr/ftr strip │
|
|
513
|
+
│ T10 strip web │ ◀ NEW (D2)
|
|
514
|
+
│ fragments │
|
|
515
|
+
│ T6 list/code restore │
|
|
516
|
+
│ T8 strip cover │ ◀ NEW (D3)
|
|
517
|
+
│ artifacts │
|
|
482
518
|
└────────────┬────────────┘
|
|
483
519
|
│
|
|
484
520
|
┌────────────┴────────────┐
|
|
485
521
|
│ pipeline/cleanup.py │ shared lane
|
|
486
|
-
│
|
|
487
|
-
│
|
|
488
|
-
│
|
|
489
|
-
│
|
|
490
|
-
│
|
|
491
|
-
│
|
|
492
|
-
│
|
|
522
|
+
│ C1 NFC normalize │ (always last)
|
|
523
|
+
│ C2 soft hyphen strip │
|
|
524
|
+
│ C3 ligature normalize │
|
|
525
|
+
│ C4 quote/dash normalize│
|
|
526
|
+
│ C5 whitespace collapse │
|
|
527
|
+
│ C8 decode HTML entities│ ◀ NEW (B1, all profiles)
|
|
528
|
+
│ C6 footnote marker strip│
|
|
529
|
+
│ C7 validate │
|
|
493
530
|
└────────────┬────────────┘
|
|
494
531
|
│
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
532
|
+
│ ┌──────────────────────────┐
|
|
533
|
+
│ │ any2md/heuristics.py │ ◀ NEW v1.0.2
|
|
534
|
+
│ │ - refine_title │
|
|
535
|
+
│ │ - refine_abstract │
|
|
536
|
+
│ │ - extract_authors │
|
|
537
|
+
│ │ (+ optional arxiv API)│
|
|
538
|
+
│ │ - filter_organization │
|
|
539
|
+
│ └────────────┬─────────────┘
|
|
540
|
+
│ │
|
|
541
|
+
▼ │
|
|
542
|
+
┌─────────────────────────┐ │
|
|
543
|
+
│ frontmatter.py │ ◀─────────┘ (consults heuristics
|
|
544
|
+
│ - merge user overrides │ before YAML emission)
|
|
498
545
|
│ - derive title/date/… │
|
|
546
|
+
│ - emit produced_by │ ◀ NEW v1.0.2
|
|
499
547
|
│ - compute content_hash │
|
|
500
548
|
│ - emit YAML │
|
|
501
549
|
└────────────┬────────────┘
|
|
@@ -504,7 +552,7 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
504
552
|
<output>.md ← frontmatter + cleaned body
|
|
505
553
|
```
|
|
506
554
|
|
|
507
|
-
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
555
|
+
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. v1.0.2 added a leaf `heuristics.py` module that `frontmatter.compose()` consults to refine the title, abstract, authors, and organization fields before YAML emission; new pipeline stages C8 and T7–T10 address concrete artifacts found in academic and standards-document PDFs. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
508
556
|
|
|
509
557
|
## Migrating from v0.7
|
|
510
558
|
|
|
@@ -1,7 +1,27 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.png" alt="any2md" width="500"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# any2md
|
|
2
6
|
|
|
3
7
|
Convert PDFs, DOCX, HTML, URLs, and plain text into structured, machine-consumable Markdown for downstream RAG pipelines.
|
|
4
8
|
|
|
9
|
+
## What's new in 1.0.2
|
|
10
|
+
|
|
11
|
+
A patch release focused on frontmatter and body-content quality, validated against four representative inputs (an arxiv academic PDF, an ISO/IEC standard, an academic DOCX, a Wikipedia URL). Eleven distinct artifacts are addressed:
|
|
12
|
+
|
|
13
|
+
- New `any2md/heuristics.py` module — pure-functions library that `frontmatter.compose()` consults to refine `title`, `abstract_for_rag`, `authors`, and `organization` before emission.
|
|
14
|
+
- New `produced_by` extension field, sitting between `extracted_via` and `pages`. Records the software that produced the source file (PDF `Creator`, DOCX `Application`). Distinct from `extracted_via`, which records the any2md backend.
|
|
15
|
+
- New shared cleanup stage **C8 `decode_html_entities`** — universal removal of `&`, `<`, `>`, and numeric entities (`—`, `—`) from body text. Code-block aware.
|
|
16
|
+
- Four new text-lane stages (aggressive/maximum profile only):
|
|
17
|
+
- **T7 `dedupe_toc_table`** — strips table-formatted TOCs that the existing T4 text-formatted heuristic doesn't catch. Common on academic PDFs.
|
|
18
|
+
- **T8 `strip_cover_artifacts`** — drops cover-page noise (QR-code blurbs, "Third edition 2022-02"-style version stamps) that appears before the first H2.
|
|
19
|
+
- **T9 `strip_repeated_byline`** — removes "Author's Contact Information:" and similar lines that duplicate a byline.
|
|
20
|
+
- **T10 `strip_web_fragments`** — drops trafilatura extraction fragments (orphan `|` and `>` lines, short incomplete sentences surrounded by blank lines).
|
|
21
|
+
- New CLI flag `--no-arxiv-lookup`. Arxiv enrichment for filenames matching `\d{4}\.\d{4,5}` is **on by default**; this flag disables it for airgapped environments.
|
|
22
|
+
|
|
23
|
+
The full design rationale lives in [docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md](docs/superpowers/specs/2026-04-26-any2md-v1.0.2-design.md). Per-fix detail is in the [CHANGELOG](CHANGELOG.md).
|
|
24
|
+
|
|
5
25
|
## What this is
|
|
6
26
|
|
|
7
27
|
any2md ingests heterogeneous source documents and emits Markdown with a fixed YAML frontmatter contract. The frontmatter shape is SSRM-compatible (Structured Security Reasoning Markdown — a documented schema for LLM-consumable documents), so retrieval pipelines, embeddings jobs, and chunking utilities downstream can rely on field names, types, and a deterministic `content_hash` for cache invalidation. The body is NFC-normalized with LF line endings, and the heading hierarchy is guaranteed to start at H1 and not skip levels. The goal is one stable shape, regardless of whether a document started life as a scanned PDF, a Word doc, a Wikipedia page, or a plain text dump.
|
|
@@ -114,6 +134,7 @@ source_file: "safety-alignment.docx" # original filename (any2md e
|
|
|
114
134
|
type: "docx" # source format (any2md extension field)
|
|
115
135
|
word_count: 14289 # any2md extension field
|
|
116
136
|
extracted_via: "docling" # which backend produced the markdown
|
|
137
|
+
produced_by: "Microsoft® Word for Microsoft 365" # software that produced the source (new in v1.0.2)
|
|
117
138
|
---
|
|
118
139
|
```
|
|
119
140
|
|
|
@@ -196,7 +217,7 @@ You'd run this when your corpus has multi-column PDFs, complex tables, or scanne
|
|
|
196
217
|
pip install "any2md[high-fidelity]"
|
|
197
218
|
```
|
|
198
219
|
|
|
199
|
-
After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`.
|
|
220
|
+
After install, the CLI auto-detects Docling and uses it for PDF and DOCX. To force it explicitly (and fail fast when it isn't installed), pass `-H` / `--high-fidelity`. To require it (treat fallback as an error), combine with `--strict`. Use `--backend pymupdf4llm` to force the lightweight backend even when Docling is installed (useful when Docling produces extraction artifacts on a specific input).
|
|
200
221
|
|
|
201
222
|
### When do I need high-fidelity?
|
|
202
223
|
|
|
@@ -378,6 +399,10 @@ type_code = "GD"
|
|
|
378
399
|
|
|
379
400
|
With this file in place and `--auto-id` passed, generated `document_id` values look like `CSA-2026-GD-a3f1c91d`. The resolution order, highest priority first: explicit `--meta KEY=VAL` arguments, then `--meta-file PATH` (when given), then auto-discovered `.any2md.toml`, then the tool's defaults. Full flag and config reference in [docs/cli-reference.md](docs/cli-reference.md).
|
|
380
401
|
|
|
402
|
+
### Network behavior
|
|
403
|
+
|
|
404
|
+
For PDFs whose filename matches the arxiv ID pattern (`\d{4}\.\d{4,5}`, e.g. `2501.17755v1.pdf`), any2md by default queries `https://export.arxiv.org/api/query` to enrich `authors`, `abstract_for_rag`, and `date` from the official arxiv metadata. The lookup is SSRF-guarded, has a 5-second timeout, and emits a non-blocking warning on any failure — conversion proceeds either way. Pass `--no-arxiv-lookup` to disable this for airgapped environments or when you want any2md to make no outbound calls. See [docs/cli-reference.md](docs/cli-reference.md#--no-arxiv-lookup) for details.
|
|
405
|
+
|
|
381
406
|
### Output verbosity and exit codes
|
|
382
407
|
|
|
383
408
|
The CLI has three verbosity flags that pair with a four-value exit-code contract:
|
|
@@ -412,7 +437,7 @@ The full symptom-cause-fix triage guide, plus deeper diagnosis steps and follow-
|
|
|
412
437
|
any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy backends (Docling), one for backends that need heavier text repair (pymupdf4llm, trafilatura, mammoth, the TXT heuristic). Both lanes converge on a shared cleanup pass, and the same `frontmatter.py` module emits YAML for every input format.
|
|
413
438
|
|
|
414
439
|
```
|
|
415
|
-
any2md v1.0
|
|
440
|
+
any2md v1.0.2
|
|
416
441
|
│
|
|
417
442
|
┌────┴────┐
|
|
418
443
|
CLI │ cli.py │ parse args, classify URL/file/dir,
|
|
@@ -429,43 +454,66 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
429
454
|
│ pymupdf4llm│ mammoth+md │ │
|
|
430
455
|
└────┬───────┴────┬───────┴───┬───┘
|
|
431
456
|
│ │ │
|
|
457
|
+
│ SourceMeta carries:
|
|
458
|
+
│ organization, produced_by ◀ NEW in v1.0.2
|
|
459
|
+
│ (split via heuristics.filter_organization)
|
|
460
|
+
│
|
|
432
461
|
structured-md structured-md text-md
|
|
433
462
|
│ │ │
|
|
434
463
|
└─────┬──────┴───────────┘
|
|
435
464
|
▼
|
|
436
465
|
┌─────────────────────────┐
|
|
437
466
|
│ pipeline/structured.py │ structured lane
|
|
438
|
-
│
|
|
439
|
-
│
|
|
440
|
-
│
|
|
441
|
-
│
|
|
467
|
+
│ S1 figure caption pass │ (trusts layout)
|
|
468
|
+
│ S2 table compactor │
|
|
469
|
+
│ S3 cite normalizer │
|
|
470
|
+
│ S4 heading hierarchy │
|
|
442
471
|
└────────────┬────────────┘
|
|
443
472
|
│
|
|
444
473
|
┌────────────┴────────────┐
|
|
445
474
|
│ pipeline/text.py │ text lane
|
|
446
|
-
│
|
|
447
|
-
│
|
|
448
|
-
│
|
|
449
|
-
│
|
|
450
|
-
│
|
|
451
|
-
│
|
|
475
|
+
│ T1 line-wrap repair │
|
|
476
|
+
│ T2 dehyphenate │
|
|
477
|
+
│ T9 strip repeated │ ◀ NEW (E1)
|
|
478
|
+
│ byline │
|
|
479
|
+
│ T3 dedupe paragraphs │
|
|
480
|
+
│ T4 dedupe TOC block │
|
|
481
|
+
│ T7 dedupe TOC table │ ◀ NEW (D1)
|
|
482
|
+
│ T5 hdr/ftr strip │
|
|
483
|
+
│ T10 strip web │ ◀ NEW (D2)
|
|
484
|
+
│ fragments │
|
|
485
|
+
│ T6 list/code restore │
|
|
486
|
+
│ T8 strip cover │ ◀ NEW (D3)
|
|
487
|
+
│ artifacts │
|
|
452
488
|
└────────────┬────────────┘
|
|
453
489
|
│
|
|
454
490
|
┌────────────┴────────────┐
|
|
455
491
|
│ pipeline/cleanup.py │ shared lane
|
|
456
|
-
│
|
|
457
|
-
│
|
|
458
|
-
│
|
|
459
|
-
│
|
|
460
|
-
│
|
|
461
|
-
│
|
|
462
|
-
│
|
|
492
|
+
│ C1 NFC normalize │ (always last)
|
|
493
|
+
│ C2 soft hyphen strip │
|
|
494
|
+
│ C3 ligature normalize │
|
|
495
|
+
│ C4 quote/dash normalize│
|
|
496
|
+
│ C5 whitespace collapse │
|
|
497
|
+
│ C8 decode HTML entities│ ◀ NEW (B1, all profiles)
|
|
498
|
+
│ C6 footnote marker strip│
|
|
499
|
+
│ C7 validate │
|
|
463
500
|
└────────────┬────────────┘
|
|
464
501
|
│
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
502
|
+
│ ┌──────────────────────────┐
|
|
503
|
+
│ │ any2md/heuristics.py │ ◀ NEW v1.0.2
|
|
504
|
+
│ │ - refine_title │
|
|
505
|
+
│ │ - refine_abstract │
|
|
506
|
+
│ │ - extract_authors │
|
|
507
|
+
│ │ (+ optional arxiv API)│
|
|
508
|
+
│ │ - filter_organization │
|
|
509
|
+
│ └────────────┬─────────────┘
|
|
510
|
+
│ │
|
|
511
|
+
▼ │
|
|
512
|
+
┌─────────────────────────┐ │
|
|
513
|
+
│ frontmatter.py │ ◀─────────┘ (consults heuristics
|
|
514
|
+
│ - merge user overrides │ before YAML emission)
|
|
468
515
|
│ - derive title/date/… │
|
|
516
|
+
│ - emit produced_by │ ◀ NEW v1.0.2
|
|
469
517
|
│ - compute content_hash │
|
|
470
518
|
│ - emit YAML │
|
|
471
519
|
└────────────┬────────────┘
|
|
@@ -474,7 +522,7 @@ any2md uses a two-lane post-processing pipeline: one lane for layout-trustworthy
|
|
|
474
522
|
<output>.md ← frontmatter + cleaned body
|
|
475
523
|
```
|
|
476
524
|
|
|
477
|
-
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
525
|
+
The two-lane design exists because Docling output is layout-correct (running aggressive line-wrap repair on it would corrupt tables), while pymupdf4llm/trafilatura/TXT output benefits from heavier regex repair. The shared cleanup pass runs identically for both lanes — that's the determinism boundary that makes `content_hash` reproducible. v1.0.2 added a leaf `heuristics.py` module that `frontmatter.compose()` consults to refine the title, abstract, authors, and organization fields before YAML emission; new pipeline stages C8 and T7–T10 address concrete artifacts found in academic and standards-document PDFs. Full stage-by-stage contract reference, plus contributor guidance for adding new converters or pipeline stages, lives in [docs/architecture.md](docs/architecture.md).
|
|
478
526
|
|
|
479
527
|
## Migrating from v0.7
|
|
480
528
|
|
|
@@ -132,6 +132,24 @@ def main():
|
|
|
132
132
|
action="store_true",
|
|
133
133
|
help="Force the Docling backend (PDF/DOCX). Exit 1 if not installed.",
|
|
134
134
|
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--no-arxiv-lookup",
|
|
137
|
+
action="store_true",
|
|
138
|
+
help="Disable the arxiv API lookup that enriches frontmatter for "
|
|
139
|
+
"PDFs whose filename matches the arxiv ID pattern (\\d{4}.\\d{4,5}). "
|
|
140
|
+
"Useful in airgapped envs or when you don't want any2md to make "
|
|
141
|
+
"network calls. Default: arxiv lookup is enabled.",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--backend",
|
|
145
|
+
choices=("docling", "pymupdf4llm", "mammoth"),
|
|
146
|
+
default=None,
|
|
147
|
+
help="Force a specific extraction backend. By default any2md auto-selects "
|
|
148
|
+
"(prefers Docling when installed). Use 'docling' (equivalent to --high-fidelity), "
|
|
149
|
+
"'pymupdf4llm' to force the lightweight PDF fallback even when Docling is installed, "
|
|
150
|
+
"or 'mammoth' to force the lightweight DOCX fallback. Mismatched format/backend "
|
|
151
|
+
"combinations (e.g., --backend pymupdf4llm on a DOCX) error out per file.",
|
|
152
|
+
)
|
|
135
153
|
parser.add_argument(
|
|
136
154
|
"--ocr-figures",
|
|
137
155
|
action="store_true",
|
|
@@ -228,11 +246,16 @@ def main():
|
|
|
228
246
|
sys.exit(1)
|
|
229
247
|
overrides = _deep_merge_overrides(overrides, cli_meta)
|
|
230
248
|
|
|
231
|
-
if
|
|
249
|
+
if (
|
|
250
|
+
args.high_fidelity
|
|
251
|
+
or args.ocr_figures
|
|
252
|
+
or args.save_images
|
|
253
|
+
or args.backend == "docling"
|
|
254
|
+
):
|
|
232
255
|
from any2md._docling import has_docling, INSTALL_HINT_MSG
|
|
233
256
|
if not has_docling():
|
|
234
257
|
print(
|
|
235
|
-
f" ERROR: Docling required for --high-fidelity / --ocr-figures / --save-images.\n"
|
|
258
|
+
f" ERROR: Docling required for --high-fidelity / --ocr-figures / --save-images / --backend docling.\n"
|
|
236
259
|
f" {INSTALL_HINT_MSG}",
|
|
237
260
|
file=sys.stderr,
|
|
238
261
|
)
|
|
@@ -252,6 +275,8 @@ def main():
|
|
|
252
275
|
auto_id_prefix=auto_id_prefix,
|
|
253
276
|
auto_id_type_code=auto_id_type_code,
|
|
254
277
|
frontmatter_overrides=overrides or None,
|
|
278
|
+
backend=args.backend,
|
|
279
|
+
arxiv_lookup=not args.no_arxiv_lookup,
|
|
255
280
|
)
|
|
256
281
|
|
|
257
282
|
# CLI-only output controls (not part of PipelineOptions).
|
|
@@ -22,6 +22,7 @@ from any2md import pipeline
|
|
|
22
22
|
from any2md._docling import has_docling
|
|
23
23
|
from any2md.converters import add_warnings, is_quiet
|
|
24
24
|
from any2md.frontmatter import SourceMeta, compose
|
|
25
|
+
from any2md.heuristics import filter_organization
|
|
25
26
|
from any2md.pipeline import PipelineOptions
|
|
26
27
|
from any2md.utils import sanitize_filename
|
|
27
28
|
|
|
@@ -40,6 +41,7 @@ def _read_docx_metadata(docx_path: Path) -> dict[str, object]:
|
|
|
40
41
|
"title_hint": None,
|
|
41
42
|
"authors": [],
|
|
42
43
|
"organization": None,
|
|
44
|
+
"produced_by": None,
|
|
43
45
|
"date": None,
|
|
44
46
|
"keywords": [],
|
|
45
47
|
}
|
|
@@ -65,8 +67,22 @@ def _read_docx_metadata(docx_path: Path) -> dict[str, object]:
|
|
|
65
67
|
with z.open("docProps/app.xml") as f:
|
|
66
68
|
root = ET.parse(f).getroot()
|
|
67
69
|
company = root.findtext("ext:Company", namespaces=_NS_APP)
|
|
68
|
-
|
|
70
|
+
application = root.findtext("ext:Application", namespaces=_NS_APP)
|
|
71
|
+
# v1.0.2: Company takes priority for `organization` (real
|
|
72
|
+
# legal entity); Application is software → goes to
|
|
73
|
+
# `produced_by` via filter_organization. When Company is
|
|
74
|
+
# absent we route Application through filter_organization
|
|
75
|
+
# for both fields so a real-org Application name still
|
|
76
|
+
# populates `organization`.
|
|
77
|
+
if company and company.strip():
|
|
69
78
|
out["organization"] = company.strip()
|
|
79
|
+
if application and application.strip():
|
|
80
|
+
app_result = filter_organization(application.strip())
|
|
81
|
+
out["produced_by"] = app_result.produced_by
|
|
82
|
+
elif application and application.strip():
|
|
83
|
+
app_result = filter_organization(application.strip())
|
|
84
|
+
out["organization"] = app_result.organization
|
|
85
|
+
out["produced_by"] = app_result.produced_by
|
|
70
86
|
except KeyError:
|
|
71
87
|
pass
|
|
72
88
|
except (zipfile.BadZipFile, ET.ParseError):
|
|
@@ -114,7 +130,24 @@ def convert_docx(
|
|
|
114
130
|
return True
|
|
115
131
|
|
|
116
132
|
try:
|
|
117
|
-
|
|
133
|
+
# Backend override (PipelineOptions.backend). When set, honor the
|
|
134
|
+
# caller's choice instead of the auto-select. ``pymupdf4llm`` is
|
|
135
|
+
# invalid for DOCX — fail the file with a clear error.
|
|
136
|
+
if options.backend == "pymupdf4llm":
|
|
137
|
+
print(
|
|
138
|
+
f" FAIL: {docx_path.name} -- backend 'pymupdf4llm' is not "
|
|
139
|
+
f"valid for DOCX input (pymupdf4llm processes PDF only).",
|
|
140
|
+
file=sys.stderr,
|
|
141
|
+
)
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
if options.backend == "mammoth":
|
|
145
|
+
md_text, extracted_via = _extract_via_mammoth(docx_path, options)
|
|
146
|
+
lane = "text"
|
|
147
|
+
elif options.backend == "docling":
|
|
148
|
+
md_text, extracted_via = _extract_via_docling(docx_path)
|
|
149
|
+
lane = "structured"
|
|
150
|
+
elif has_docling():
|
|
118
151
|
try:
|
|
119
152
|
md_text, extracted_via = _extract_via_docling(docx_path)
|
|
120
153
|
lane = "structured"
|
|
@@ -150,6 +183,7 @@ def convert_docx(
|
|
|
150
183
|
doc_type="docx",
|
|
151
184
|
extracted_via=extracted_via,
|
|
152
185
|
lane=lane,
|
|
186
|
+
produced_by=props["produced_by"],
|
|
153
187
|
)
|
|
154
188
|
full = compose(
|
|
155
189
|
md_text, meta, options, overrides=options.frontmatter_overrides
|
|
@@ -20,6 +20,7 @@ from any2md import pipeline
|
|
|
20
20
|
from any2md._docling import has_docling, install_hint
|
|
21
21
|
from any2md.converters import add_warnings, is_quiet
|
|
22
22
|
from any2md.frontmatter import SourceMeta, compose
|
|
23
|
+
from any2md.heuristics import filter_organization
|
|
23
24
|
from any2md.pipeline import PipelineOptions
|
|
24
25
|
from any2md.utils import sanitize_filename
|
|
25
26
|
|
|
@@ -47,10 +48,13 @@ def _parse_pdf_date(raw: str | None) -> str | None:
|
|
|
47
48
|
|
|
48
49
|
def _parse_pdf_metadata(doc: "pymupdf.Document") -> dict[str, object]:
|
|
49
50
|
meta = doc.metadata or {}
|
|
51
|
+
creator_raw = (meta.get("creator") or "").strip() or None
|
|
52
|
+
org_result = filter_organization(creator_raw)
|
|
50
53
|
return {
|
|
51
54
|
"title_hint": (meta.get("title") or "").strip() or None,
|
|
52
55
|
"authors": _parse_pdf_authors(meta.get("author")),
|
|
53
|
-
"organization":
|
|
56
|
+
"organization": org_result.organization,
|
|
57
|
+
"produced_by": org_result.produced_by,
|
|
54
58
|
"date": _parse_pdf_date(meta.get("creationDate")),
|
|
55
59
|
"keywords": [
|
|
56
60
|
k.strip() for k in (meta.get("keywords") or "").split(",") if k.strip()
|
|
@@ -182,9 +186,31 @@ def convert_pdf(
|
|
|
182
186
|
return True
|
|
183
187
|
|
|
184
188
|
try:
|
|
185
|
-
|
|
189
|
+
# Backend override (PipelineOptions.backend). When set, honor the
|
|
190
|
+
# caller's choice instead of the auto-select. ``mammoth`` is
|
|
191
|
+
# invalid for PDFs — fail the file with a clear error.
|
|
192
|
+
if options.backend == "mammoth":
|
|
193
|
+
print(
|
|
194
|
+
f" FAIL: {pdf_path.name} -- backend 'mammoth' is not "
|
|
195
|
+
f"valid for PDF input (mammoth processes DOCX only).",
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
if options.backend == "pymupdf4llm":
|
|
201
|
+
use_docling = False
|
|
202
|
+
elif options.backend == "docling":
|
|
203
|
+
# Same install-required behavior as --high-fidelity. If
|
|
204
|
+
# Docling isn't importable, fall through to the existing
|
|
205
|
+
# auto-select path which will error via _extract_via_docling
|
|
206
|
+
# below if Docling truly isn't usable. The CLI layer
|
|
207
|
+
# pre-checks install for both --high-fidelity and
|
|
208
|
+
# --backend docling, so this branch normally has Docling.
|
|
209
|
+
use_docling = has_docling()
|
|
210
|
+
else:
|
|
211
|
+
use_docling = has_docling()
|
|
186
212
|
|
|
187
|
-
if not use_docling and pdf_looks_complex(pdf_path):
|
|
213
|
+
if not use_docling and options.backend is None and pdf_looks_complex(pdf_path):
|
|
188
214
|
install_hint()
|
|
189
215
|
|
|
190
216
|
# Always extract metadata via PyMuPDF — independent of which
|
|
@@ -234,6 +260,7 @@ def convert_pdf(
|
|
|
234
260
|
doc_type="pdf",
|
|
235
261
|
extracted_via=extracted_via,
|
|
236
262
|
lane=lane,
|
|
263
|
+
produced_by=props["produced_by"],
|
|
237
264
|
)
|
|
238
265
|
full = compose(
|
|
239
266
|
md_text, meta, options, overrides=options.frontmatter_overrides
|