raw2md 0.1.0__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.
Files changed (242) hide show
  1. raw2md-0.1.0/.gitattributes +34 -0
  2. raw2md-0.1.0/.github/scripts/check-smoke-result.sh +35 -0
  3. raw2md-0.1.0/.github/workflows/ci.yml +123 -0
  4. raw2md-0.1.0/.github/workflows/docs.yml +41 -0
  5. raw2md-0.1.0/.github/workflows/release.yml +123 -0
  6. raw2md-0.1.0/.github/workflows/smoke.yml +179 -0
  7. raw2md-0.1.0/.gitignore +37 -0
  8. raw2md-0.1.0/.markdownlint-cli2.jsonc +31 -0
  9. raw2md-0.1.0/AGENTS.md +119 -0
  10. raw2md-0.1.0/LICENSE +21 -0
  11. raw2md-0.1.0/PKG-INFO +322 -0
  12. raw2md-0.1.0/README.md +280 -0
  13. raw2md-0.1.0/THIRD_PARTY_NOTICES.md +87 -0
  14. raw2md-0.1.0/benchmarks/README.md +35 -0
  15. raw2md-0.1.0/benchmarks/mpdocbench/README.md +42 -0
  16. raw2md-0.1.0/benchmarks/mpdocbench/results/all.yaml +68 -0
  17. raw2md-0.1.0/benchmarks/mpdocbench/results/english.yaml +68 -0
  18. raw2md-0.1.0/benchmarks/mpdocbench/run.sh +30 -0
  19. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/__init__.py +1 -0
  20. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/cli.py +190 -0
  21. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/convert.py +92 -0
  22. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/fetch.py +255 -0
  23. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/pdfs.py +81 -0
  24. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/pins.py +217 -0
  25. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/report.py +60 -0
  26. raw2md-0.1.0/benchmarks/mpdocbench/scripts/mpdocbench/scoring.py +415 -0
  27. raw2md-0.1.0/benchmarks/mpdocbench/scripts/pins.yaml +66 -0
  28. raw2md-0.1.0/benchmarks/mpdocbench/scripts/run.py +6 -0
  29. raw2md-0.1.0/benchmarks/mpdocbench/scripts/scorer/requirements.txt +27 -0
  30. raw2md-0.1.0/benchmarks/mpdocbench/scripts/scorer/setup.sh +175 -0
  31. raw2md-0.1.0/codecov.yml +10 -0
  32. raw2md-0.1.0/docs/README.md +21 -0
  33. raw2md-0.1.0/docs/assets/raw2md-logo.svg +1 -0
  34. raw2md-0.1.0/docs/cleaning.md +296 -0
  35. raw2md-0.1.0/docs/inspection.md +166 -0
  36. raw2md-0.1.0/docs/post.md +175 -0
  37. raw2md-0.1.0/docs/quality.md +179 -0
  38. raw2md-0.1.0/docs/reference.md +433 -0
  39. raw2md-0.1.0/examples/MODEL_LICENSE +59 -0
  40. raw2md-0.1.0/examples/README.md +58 -0
  41. raw2md-0.1.0/examples/book.md +3877 -0
  42. raw2md-0.1.0/examples/report.md +11241 -0
  43. raw2md-0.1.0/examples/thesis.md +2918 -0
  44. raw2md-0.1.0/notes/notes.md +14 -0
  45. raw2md-0.1.0/package-lock.json +1347 -0
  46. raw2md-0.1.0/package.json +12 -0
  47. raw2md-0.1.0/pyproject.toml +223 -0
  48. raw2md-0.1.0/src/.gitkeep +0 -0
  49. raw2md-0.1.0/src/raw2md/__init__.py +1 -0
  50. raw2md-0.1.0/src/raw2md/__main__.py +5 -0
  51. raw2md-0.1.0/src/raw2md/cleaner.py +389 -0
  52. raw2md-0.1.0/src/raw2md/cleaning/__init__.py +169 -0
  53. raw2md-0.1.0/src/raw2md/cleaning/findings.py +640 -0
  54. raw2md-0.1.0/src/raw2md/cleaning/headings.py +2506 -0
  55. raw2md-0.1.0/src/raw2md/cleaning/lists.py +348 -0
  56. raw2md-0.1.0/src/raw2md/cleaning/math.py +2528 -0
  57. raw2md-0.1.0/src/raw2md/cleaning/ocr.py +354 -0
  58. raw2md-0.1.0/src/raw2md/cleaning/options.py +36 -0
  59. raw2md-0.1.0/src/raw2md/cleaning/prose.py +1513 -0
  60. raw2md-0.1.0/src/raw2md/cleaning/segments.py +357 -0
  61. raw2md-0.1.0/src/raw2md/cleaning/tables.py +1661 -0
  62. raw2md-0.1.0/src/raw2md/cli.py +240 -0
  63. raw2md-0.1.0/src/raw2md/config.py +113 -0
  64. raw2md-0.1.0/src/raw2md/doctor.py +222 -0
  65. raw2md-0.1.0/src/raw2md/engines/__init__.py +40 -0
  66. raw2md-0.1.0/src/raw2md/engines/base.py +179 -0
  67. raw2md-0.1.0/src/raw2md/engines/djvu.py +309 -0
  68. raw2md-0.1.0/src/raw2md/engines/marker.py +273 -0
  69. raw2md-0.1.0/src/raw2md/engines/pandoc.py +206 -0
  70. raw2md-0.1.0/src/raw2md/engines/pandoc_tables.py +187 -0
  71. raw2md-0.1.0/src/raw2md/engines/preflight.py +56 -0
  72. raw2md-0.1.0/src/raw2md/engines/pymupdf.py +153 -0
  73. raw2md-0.1.0/src/raw2md/engines/signature.py +81 -0
  74. raw2md-0.1.0/src/raw2md/exit_codes.py +16 -0
  75. raw2md-0.1.0/src/raw2md/header.py +281 -0
  76. raw2md-0.1.0/src/raw2md/init.py +147 -0
  77. raw2md-0.1.0/src/raw2md/katex.min.js +1 -0
  78. raw2md-0.1.0/src/raw2md/katex.py +93 -0
  79. raw2md-0.1.0/src/raw2md/keywords.py +251 -0
  80. raw2md-0.1.0/src/raw2md/keywords.yaml +111 -0
  81. raw2md-0.1.0/src/raw2md/llm/__init__.py +58 -0
  82. raw2md-0.1.0/src/raw2md/llm/acceptance.py +196 -0
  83. raw2md-0.1.0/src/raw2md/llm/base.py +327 -0
  84. raw2md-0.1.0/src/raw2md/llm/chunking.py +427 -0
  85. raw2md-0.1.0/src/raw2md/llm/claude.py +189 -0
  86. raw2md-0.1.0/src/raw2md/llm/edit_guard.py +816 -0
  87. raw2md-0.1.0/src/raw2md/llm/factory.py +25 -0
  88. raw2md-0.1.0/src/raw2md/llm/gemini.py +475 -0
  89. raw2md-0.1.0/src/raw2md/llm/inspection/__init__.py +11 -0
  90. raw2md-0.1.0/src/raw2md/llm/inspection/addressing.py +460 -0
  91. raw2md-0.1.0/src/raw2md/llm/inspection/apply.py +369 -0
  92. raw2md-0.1.0/src/raw2md/llm/inspection/common.py +73 -0
  93. raw2md-0.1.0/src/raw2md/llm/inspection/coordinator.py +410 -0
  94. raw2md-0.1.0/src/raw2md/llm/inspection/masking.py +272 -0
  95. raw2md-0.1.0/src/raw2md/llm/inspection/numbering.py +216 -0
  96. raw2md-0.1.0/src/raw2md/llm/inspection/reply.py +302 -0
  97. raw2md-0.1.0/src/raw2md/llm/ocr.py +196 -0
  98. raw2md-0.1.0/src/raw2md/llm/post/__init__.py +9 -0
  99. raw2md-0.1.0/src/raw2md/llm/post/blocks.py +820 -0
  100. raw2md-0.1.0/src/raw2md/llm/post/common.py +186 -0
  101. raw2md-0.1.0/src/raw2md/llm/post/coordinator.py +341 -0
  102. raw2md-0.1.0/src/raw2md/llm/post/ladder.py +609 -0
  103. raw2md-0.1.0/src/raw2md/llm/post/spacing.py +341 -0
  104. raw2md-0.1.0/src/raw2md/llm/post/tables.py +599 -0
  105. raw2md-0.1.0/src/raw2md/llm/rpd.py +102 -0
  106. raw2md-0.1.0/src/raw2md/llm/trace.py +98 -0
  107. raw2md-0.1.0/src/raw2md/lock.py +178 -0
  108. raw2md-0.1.0/src/raw2md/logging_setup.py +188 -0
  109. raw2md-0.1.0/src/raw2md/mdtext/__init__.py +12 -0
  110. raw2md-0.1.0/src/raw2md/mdtext/formulas.py +355 -0
  111. raw2md-0.1.0/src/raw2md/mdtext/lines.py +93 -0
  112. raw2md-0.1.0/src/raw2md/mdtext/links.py +67 -0
  113. raw2md-0.1.0/src/raw2md/mdtext/loops.py +436 -0
  114. raw2md-0.1.0/src/raw2md/mdtext/math_spans.py +118 -0
  115. raw2md-0.1.0/src/raw2md/mdtext/pages.py +65 -0
  116. raw2md-0.1.0/src/raw2md/mdtext/tables.py +490 -0
  117. raw2md-0.1.0/src/raw2md/mdtext/zones.py +190 -0
  118. raw2md-0.1.0/src/raw2md/media.py +379 -0
  119. raw2md-0.1.0/src/raw2md/orchestrator.py +535 -0
  120. raw2md-0.1.0/src/raw2md/output.py +229 -0
  121. raw2md-0.1.0/src/raw2md/paths.py +47 -0
  122. raw2md-0.1.0/src/raw2md/pipeline.py +1161 -0
  123. raw2md-0.1.0/src/raw2md/progress.py +239 -0
  124. raw2md-0.1.0/src/raw2md/prompts.py +336 -0
  125. raw2md-0.1.0/src/raw2md/quality/__init__.py +5 -0
  126. raw2md-0.1.0/src/raw2md/quality/evaluator.py +1155 -0
  127. raw2md-0.1.0/src/raw2md/quality/headings.py +461 -0
  128. raw2md-0.1.0/src/raw2md/queue.py +405 -0
  129. raw2md-0.1.0/src/raw2md/service.py +62 -0
  130. raw2md-0.1.0/src/raw2md/settings.py +473 -0
  131. raw2md-0.1.0/src/raw2md/source_outline.py +141 -0
  132. raw2md-0.1.0/src/raw2md/source_text.py +196 -0
  133. raw2md-0.1.0/src/raw2md/temp.py +65 -0
  134. raw2md-0.1.0/tests/.gitkeep +0 -0
  135. raw2md-0.1.0/tests/cleaning/__init__.py +0 -0
  136. raw2md-0.1.0/tests/cleaning/_helpers.py +61 -0
  137. raw2md-0.1.0/tests/cleaning/test_findings.py +1164 -0
  138. raw2md-0.1.0/tests/cleaning/test_heading_levels.py +1958 -0
  139. raw2md-0.1.0/tests/cleaning/test_heading_recovery.py +952 -0
  140. raw2md-0.1.0/tests/cleaning/test_headings.py +567 -0
  141. raw2md-0.1.0/tests/cleaning/test_lists.py +300 -0
  142. raw2md-0.1.0/tests/cleaning/test_math.py +1683 -0
  143. raw2md-0.1.0/tests/cleaning/test_ocr.py +309 -0
  144. raw2md-0.1.0/tests/cleaning/test_prose.py +891 -0
  145. raw2md-0.1.0/tests/cleaning/test_prose_rejoin.py +884 -0
  146. raw2md-0.1.0/tests/cleaning/test_tables.py +1940 -0
  147. raw2md-0.1.0/tests/conftest.py +119 -0
  148. raw2md-0.1.0/tests/corpus/README.md +130 -0
  149. raw2md-0.1.0/tests/corpus/assets/chart-2.png +0 -0
  150. raw2md-0.1.0/tests/corpus/assets/chart-3.png +0 -0
  151. raw2md-0.1.0/tests/corpus/assets/chart.png +0 -0
  152. raw2md-0.1.0/tests/corpus/assets/diagram (v2).png +0 -0
  153. raw2md-0.1.0/tests/corpus/assets/fonts/Caveat-Regular.ttf +0 -0
  154. raw2md-0.1.0/tests/corpus/assets/fonts/OFL.txt +93 -0
  155. raw2md-0.1.0/tests/corpus/assets/fonts/sources.md +16 -0
  156. raw2md-0.1.0/tests/corpus/assets/sample.png +0 -0
  157. raw2md-0.1.0/tests/corpus/chart_series.md +23 -0
  158. raw2md-0.1.0/tests/corpus/engineering_prose.md +38 -0
  159. raw2md-0.1.0/tests/corpus/footnotes.md +33 -0
  160. raw2md-0.1.0/tests/corpus/formulas.md +68 -0
  161. raw2md-0.1.0/tests/corpus/handwriting.md +38 -0
  162. raw2md-0.1.0/tests/corpus/headers.md +69 -0
  163. raw2md-0.1.0/tests/corpus/images.md +23 -0
  164. raw2md-0.1.0/tests/corpus/long_doc.md +121 -0
  165. raw2md-0.1.0/tests/corpus/math_legend.md +32 -0
  166. raw2md-0.1.0/tests/corpus/mixed.md +86 -0
  167. raw2md-0.1.0/tests/corpus/multilang.md +44 -0
  168. raw2md-0.1.0/tests/corpus/multiline_display_math.md +15 -0
  169. raw2md-0.1.0/tests/corpus/nested_lists.md +62 -0
  170. raw2md-0.1.0/tests/corpus/single_glyph_math.md +12 -0
  171. raw2md-0.1.0/tests/corpus/table_formulas.md +27 -0
  172. raw2md-0.1.0/tests/corpus/table_math_pipes.md +15 -0
  173. raw2md-0.1.0/tests/corpus/tables.md +68 -0
  174. raw2md-0.1.0/tests/fixtures/offline/prompts.yaml +11 -0
  175. raw2md-0.1.0/tests/fixtures/offline/settings.json +39 -0
  176. raw2md-0.1.0/tests/fixtures/real/prompts.yaml +147 -0
  177. raw2md-0.1.0/tests/fixtures/real/settings.json +39 -0
  178. raw2md-0.1.0/tests/inspection/__init__.py +0 -0
  179. raw2md-0.1.0/tests/inspection/_helpers.py +256 -0
  180. raw2md-0.1.0/tests/inspection/test_addressing.py +929 -0
  181. raw2md-0.1.0/tests/inspection/test_apply.py +821 -0
  182. raw2md-0.1.0/tests/inspection/test_coordinator.py +279 -0
  183. raw2md-0.1.0/tests/inspection/test_masking.py +336 -0
  184. raw2md-0.1.0/tests/inspection/test_reply.py +331 -0
  185. raw2md-0.1.0/tests/post/__init__.py +0 -0
  186. raw2md-0.1.0/tests/post/_helpers.py +147 -0
  187. raw2md-0.1.0/tests/post/test_blocks.py +741 -0
  188. raw2md-0.1.0/tests/post/test_coordinator.py +220 -0
  189. raw2md-0.1.0/tests/post/test_ladder.py +615 -0
  190. raw2md-0.1.0/tests/post/test_spacing.py +213 -0
  191. raw2md-0.1.0/tests/post/test_tables.py +748 -0
  192. raw2md-0.1.0/tests/synthesis/__init__.py +0 -0
  193. raw2md-0.1.0/tests/synthesis/_degrade.py +65 -0
  194. raw2md-0.1.0/tests/synthesis/_paths.py +17 -0
  195. raw2md-0.1.0/tests/synthesis/_structure.py +194 -0
  196. raw2md-0.1.0/tests/synthesis/gen_djvu.py +58 -0
  197. raw2md-0.1.0/tests/synthesis/gen_docx.py +37 -0
  198. raw2md-0.1.0/tests/synthesis/gen_pdf.py +70 -0
  199. raw2md-0.1.0/tests/synthesis/gen_scan.py +77 -0
  200. raw2md-0.1.0/tests/synthesis/gen_smoke.py +96 -0
  201. raw2md-0.1.0/tests/synthesis/generate.py +81 -0
  202. raw2md-0.1.0/tests/synthesis/roundtrip.py +236 -0
  203. raw2md-0.1.0/tests/synthesis/runner.py +361 -0
  204. raw2md-0.1.0/tests/synthesis/test_corpus_fast.py +368 -0
  205. raw2md-0.1.0/tests/synthesis/test_llm_live.py +345 -0
  206. raw2md-0.1.0/tests/synthesis/test_roundtrip.py +446 -0
  207. raw2md-0.1.0/tests/synthesis/test_structure.py +209 -0
  208. raw2md-0.1.0/tests/synthesis/test_synthesis.py +175 -0
  209. raw2md-0.1.0/tests/test_acceptance.py +220 -0
  210. raw2md-0.1.0/tests/test_cleaner.py +279 -0
  211. raw2md-0.1.0/tests/test_cli.py +494 -0
  212. raw2md-0.1.0/tests/test_comment_length.py +112 -0
  213. raw2md-0.1.0/tests/test_doctor.py +371 -0
  214. raw2md-0.1.0/tests/test_edit_guard.py +1219 -0
  215. raw2md-0.1.0/tests/test_engines.py +2419 -0
  216. raw2md-0.1.0/tests/test_entry_point.py +31 -0
  217. raw2md-0.1.0/tests/test_fixtures_config.py +35 -0
  218. raw2md-0.1.0/tests/test_header.py +444 -0
  219. raw2md-0.1.0/tests/test_init.py +156 -0
  220. raw2md-0.1.0/tests/test_inspection_chunking.py +1118 -0
  221. raw2md-0.1.0/tests/test_katex.py +93 -0
  222. raw2md-0.1.0/tests/test_keywords.py +281 -0
  223. raw2md-0.1.0/tests/test_llm.py +1619 -0
  224. raw2md-0.1.0/tests/test_llm_trace.py +111 -0
  225. raw2md-0.1.0/tests/test_lock.py +132 -0
  226. raw2md-0.1.0/tests/test_logging_setup.py +460 -0
  227. raw2md-0.1.0/tests/test_media.py +580 -0
  228. raw2md-0.1.0/tests/test_mpdocbench.py +599 -0
  229. raw2md-0.1.0/tests/test_ocr.py +297 -0
  230. raw2md-0.1.0/tests/test_orchestrator.py +2027 -0
  231. raw2md-0.1.0/tests/test_output.py +486 -0
  232. raw2md-0.1.0/tests/test_pipeline.py +2547 -0
  233. raw2md-0.1.0/tests/test_progress.py +347 -0
  234. raw2md-0.1.0/tests/test_prompts.py +338 -0
  235. raw2md-0.1.0/tests/test_quality.py +2200 -0
  236. raw2md-0.1.0/tests/test_queue.py +611 -0
  237. raw2md-0.1.0/tests/test_rpd.py +85 -0
  238. raw2md-0.1.0/tests/test_settings.py +592 -0
  239. raw2md-0.1.0/tests/test_source_outline.py +129 -0
  240. raw2md-0.1.0/tests/test_source_text.py +184 -0
  241. raw2md-0.1.0/tests/test_temp.py +52 -0
  242. raw2md-0.1.0/uv.lock +2502 -0
@@ -0,0 +1,34 @@
1
+ # Normalize line endings: LF in the repository and in the working tree on every
2
+ # platform. Gives deterministic, cross-platform checkouts (Windows + Linux) and
3
+ # silences the "LF will be replaced by CRLF" warning.
4
+ * text=auto eol=lf
5
+
6
+ # Scripts that must keep CRLF to run correctly on Windows.
7
+ *.bat text eol=crlf
8
+ *.cmd text eol=crlf
9
+
10
+ # Scripts that must stay LF to run on Linux.
11
+ *.sh text eol=lf
12
+
13
+ # The vendored KaTeX bundle is minified onto a single 270 KB line: a text diff
14
+ # of it prints the whole file and says nothing.
15
+ src/raw2md/katex.min.js -diff
16
+
17
+ # Binary files: never normalize line endings, never diff as text.
18
+ *.pdf binary
19
+ *.docx binary
20
+ *.doc binary
21
+ *.djvu binary
22
+ *.png binary
23
+ *.jpg binary
24
+ *.jpeg binary
25
+ *.gif binary
26
+ *.bmp binary
27
+ *.tif binary
28
+ *.tiff binary
29
+ *.ico binary
30
+ *.zip binary
31
+ *.gz binary
32
+ *.7z binary
33
+ *.ttf binary
34
+ *.otf binary
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ # Check one smoke conversion result: the file exists, opens with a YAML header
3
+ # that reports `ok`, and carries a body. The body is measured in non-blank
4
+ # characters rather than lines, because a converter is free to join or split
5
+ # lines; what separates a real conversion from an empty result that still
6
+ # parses is how much text arrived.
7
+ set -euo pipefail
8
+
9
+ result="$1"
10
+ minimum_body_chars="${2:-100}"
11
+
12
+ test -f "$result" || {
13
+ echo "no result at $result" >&2
14
+ exit 1
15
+ }
16
+
17
+ head -n 1 "$result" | grep -qx -- '---' || {
18
+ echo "result does not open with a YAML header" >&2
19
+ exit 1
20
+ }
21
+
22
+ grep -qx 'status: ok' "$result" || {
23
+ echo "result is not status: ok" >&2
24
+ sed -n '1,20p' "$result" >&2
25
+ exit 1
26
+ }
27
+
28
+ body_chars=$(awk 'body { print } /^---$/ { if (++fence == 2) body = 1 }' "$result" |
29
+ tr -d '[:space:]' | wc -c)
30
+ if [ "$body_chars" -lt "$minimum_body_chars" ]; then
31
+ echo "body holds $body_chars non-blank character(s), expected at least $minimum_body_chars" >&2
32
+ exit 1
33
+ fi
34
+
35
+ echo "$result: header ok, $body_chars non-blank body character(s)"
@@ -0,0 +1,123 @@
1
+ name: CI
2
+
3
+ on:
4
+ # No test reads these paths, and docs.yml lints the Markdown. A run holds
5
+ # four runners for minutes, and the next push queues behind it.
6
+ # tests/corpus/ Markdown is absent on purpose: the fast layer reads it. A
7
+ # new top-level folder joins the list only if no test reads it. The list is
8
+ # written twice: GitHub Actions does not resolve YAML anchors.
9
+ push:
10
+ branches: [main]
11
+ paths-ignore:
12
+ - 'docs/**'
13
+ - 'examples/**'
14
+ - 'README.md'
15
+ - 'AGENTS.md'
16
+ - 'LICENSE'
17
+ - 'THIRD_PARTY_NOTICES.md'
18
+ - '.markdownlint-cli2.jsonc'
19
+ - 'package.json'
20
+ - 'package-lock.json'
21
+ pull_request:
22
+ paths-ignore:
23
+ - 'docs/**'
24
+ - 'examples/**'
25
+ - 'README.md'
26
+ - 'AGENTS.md'
27
+ - 'LICENSE'
28
+ - 'THIRD_PARTY_NOTICES.md'
29
+ - '.markdownlint-cli2.jsonc'
30
+ - 'package.json'
31
+ - 'package-lock.json'
32
+
33
+ concurrency:
34
+ group: ci-${{ github.workflow }}-${{ github.ref }}
35
+ cancel-in-progress: true
36
+
37
+ jobs:
38
+ fast:
39
+ # Python versions fan out on fast Linux jobs, from the floor to the
40
+ # ceiling in pyproject.toml. One Windows job, the slowest of the set, runs
41
+ # the tests for the OS-specific behavior (the lock, the temp root, paths).
42
+ # The linters and the coverage report run once.
43
+ strategy:
44
+ fail-fast: false
45
+ matrix:
46
+ include:
47
+ - os: ubuntu-24.04
48
+ python: "3.11"
49
+ - os: ubuntu-24.04
50
+ python: "3.12"
51
+ lint: true
52
+ - os: ubuntu-24.04
53
+ python: "3.13"
54
+ - os: windows-2025
55
+ python: "3.12"
56
+ runs-on: ${{ matrix.os }}
57
+ # id-token lets the Codecov upload authenticate through OIDC instead of a
58
+ # repository secret. Permissions are per job, not per matrix entry, so the
59
+ # entries without coverage hold it too; none of them uses it.
60
+ permissions:
61
+ contents: read
62
+ id-token: write
63
+ env:
64
+ # The fast layer never imports torch, so the multi-GB wheel and its
65
+ # Linux-only CUDA and triton dependencies are skipped below; each is
66
+ # reachable in uv.lock only through torch. A new one after a torch
67
+ # upgrade only slows the job. UV_NO_SYNC stops `uv run` re-syncing them.
68
+ UV_NO_SYNC: "1"
69
+ steps:
70
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
71
+
72
+ - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
73
+ with:
74
+ enable-cache: true
75
+ python-version: ${{ matrix.python }}
76
+
77
+ - run: >-
78
+ uv sync --locked
79
+ --no-install-package torch
80
+ --no-install-package cuda-bindings
81
+ --no-install-package cuda-pathfinder
82
+ --no-install-package cuda-toolkit
83
+ --no-install-package nvidia-cublas
84
+ --no-install-package nvidia-cuda-cupti
85
+ --no-install-package nvidia-cuda-nvrtc
86
+ --no-install-package nvidia-cuda-runtime
87
+ --no-install-package nvidia-cudnn-cu13
88
+ --no-install-package nvidia-cufft
89
+ --no-install-package nvidia-cufile
90
+ --no-install-package nvidia-curand
91
+ --no-install-package nvidia-cusolver
92
+ --no-install-package nvidia-cusparse
93
+ --no-install-package nvidia-cusparselt-cu13
94
+ --no-install-package nvidia-nccl-cu13
95
+ --no-install-package nvidia-nvjitlink
96
+ --no-install-package nvidia-nvshmem-cu13
97
+ --no-install-package nvidia-nvtx
98
+ --no-install-package triton
99
+
100
+ - if: matrix.lint
101
+ run: uv run ruff format --check .
102
+ - if: matrix.lint
103
+ run: uv run ruff check .
104
+ - if: matrix.lint
105
+ run: uv run mypy
106
+
107
+ - if: ${{ !matrix.lint }}
108
+ run: uv run pytest
109
+
110
+ - if: matrix.lint
111
+ run: uv run pytest --cov --cov-report=xml
112
+ - if: matrix.lint
113
+ run: uv run coverage report --format=markdown --skip-covered >> "$GITHUB_STEP_SUMMARY"
114
+ - if: matrix.lint
115
+ uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
116
+ with:
117
+ use_oidc: true
118
+ files: coverage.xml
119
+ disable_search: true
120
+
121
+ # Opt-in locally, but seconds long, and the lock is OS-specific: every
122
+ # job runs it as a step of its own rather than a job billed separately.
123
+ - run: uv run pytest -m concurrency
@@ -0,0 +1,41 @@
1
+ name: Docs
2
+
3
+ # The Markdown lint alone: a prose-only commit needs nothing else, and one
4
+ # short job holds a runner for a fraction of the time ci.yml would. The paths
5
+ # are wider than those ci.yml ignores: Markdown lives under tests/ too, and a
6
+ # lint config change re-lints everything. Written twice: Actions does not
7
+ # resolve YAML anchors.
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ paths:
12
+ - '**.md'
13
+ - '.markdownlint-cli2.jsonc'
14
+ - 'package.json'
15
+ - 'package-lock.json'
16
+ - '.github/workflows/docs.yml'
17
+ pull_request:
18
+ paths:
19
+ - '**.md'
20
+ - '.markdownlint-cli2.jsonc'
21
+ - 'package.json'
22
+ - 'package-lock.json'
23
+ - '.github/workflows/docs.yml'
24
+
25
+ concurrency:
26
+ group: docs-${{ github.workflow }}-${{ github.ref }}
27
+ cancel-in-progress: true
28
+
29
+ jobs:
30
+ markdown:
31
+ runs-on: ubuntu-24.04
32
+ steps:
33
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
34
+
35
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
36
+ with:
37
+ node-version: lts/*
38
+ cache: npm
39
+
40
+ - run: npm ci
41
+ - run: npm run lint:md
@@ -0,0 +1,123 @@
1
+ name: Release
2
+
3
+ # A pushed v* tag builds once, smoke-tests the built wheel, uploads to PyPI
4
+ # and then makes the GitHub release, so a failed upload leaves no release. A
5
+ # manual run is the dry run: build and smoke only. A manual run on a tag also
6
+ # extracts the release notes, which shows that the annotation reaches the
7
+ # runner.
8
+ on:
9
+ push:
10
+ tags: ['v*']
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ concurrency:
17
+ group: release-${{ github.ref }}
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-24.04
23
+ timeout-minutes: 10
24
+ steps:
25
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
26
+
27
+ - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
28
+ with:
29
+ python-version: "3.12"
30
+
31
+ - name: Check the tag against the package version
32
+ if: github.event_name == 'push'
33
+ run: |
34
+ version=$(uv run --no-project python -c \
35
+ "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
36
+ if [ "$GITHUB_REF_NAME" != "v$version" ]; then
37
+ echo "tag $GITHUB_REF_NAME does not match version $version" >&2
38
+ exit 1
39
+ fi
40
+
41
+ - name: Extract the release notes from the tag
42
+ # The checkout may store the tag without its annotation, so the tag is
43
+ # fetched again as it is on the remote.
44
+ if: github.ref_type == 'tag'
45
+ run: |
46
+ git fetch --force --depth=1 origin "+refs/tags/$GITHUB_REF_NAME:refs/tags/$GITHUB_REF_NAME"
47
+ if [ "$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")" != "tag" ]; then
48
+ echo "tag $GITHUB_REF_NAME is not annotated; the release text comes from its message" >&2
49
+ exit 1
50
+ fi
51
+ mkdir notes
52
+ git for-each-ref --format='%(contents:subject)%0a%0a%(contents:body)' \
53
+ "refs/tags/$GITHUB_REF_NAME" > notes/notes.md
54
+ cat notes/notes.md >> "$GITHUB_STEP_SUMMARY"
55
+
56
+ # --no-sources builds what PyPI users get: without the CUDA torch pin.
57
+ - run: uv build --no-sources
58
+ - run: uvx twine check --strict dist/*
59
+
60
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
61
+ with:
62
+ name: dist
63
+ path: dist/
64
+ if-no-files-found: error
65
+
66
+ - if: github.ref_type == 'tag'
67
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
68
+ with:
69
+ name: notes
70
+ path: notes/notes.md
71
+ if-no-files-found: error
72
+
73
+ smoke:
74
+ needs: build
75
+ uses: ./.github/workflows/smoke.yml
76
+ with:
77
+ wheel-artifact: dist
78
+
79
+ pypi:
80
+ needs: smoke
81
+ if: github.event_name == 'push'
82
+ runs-on: ubuntu-24.04
83
+ timeout-minutes: 10
84
+ # The environment holds the manual approval and admits v* tags only.
85
+ environment:
86
+ name: pypi
87
+ url: https://pypi.org/project/raw2md/
88
+ permissions:
89
+ id-token: write
90
+ steps:
91
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
92
+ with:
93
+ name: dist
94
+ path: dist
95
+
96
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
97
+
98
+ github-release:
99
+ needs: pypi
100
+ runs-on: ubuntu-24.04
101
+ timeout-minutes: 10
102
+ permissions:
103
+ contents: write
104
+ steps:
105
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
106
+ with:
107
+ name: dist
108
+ path: dist
109
+
110
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
111
+ with:
112
+ name: notes
113
+ path: notes
114
+
115
+ - name: Create the release
116
+ env:
117
+ GH_TOKEN: ${{ github.token }}
118
+ run: >-
119
+ gh release create "$GITHUB_REF_NAME" dist/*
120
+ --repo "$GITHUB_REPOSITORY"
121
+ --title "$GITHUB_REF_NAME"
122
+ --notes-file notes/notes.md
123
+ --verify-tag
@@ -0,0 +1,179 @@
1
+ name: Smoke
2
+
3
+ # It installs the tool as a user does, with CPU torch. A dependency release
4
+ # breaks it more often than a commit does, hence the weekly run; a push runs it
5
+ # only when the installed code or the smoke itself changes. The release calls
6
+ # it on the wheel it built.
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ paths:
11
+ - 'src/**'
12
+ - 'pyproject.toml'
13
+ - '.github/workflows/smoke.yml'
14
+ - '.github/scripts/**'
15
+ - 'tests/synthesis/gen_smoke.py'
16
+ schedule:
17
+ - cron: '17 5 * * 1'
18
+ workflow_dispatch:
19
+ workflow_call:
20
+ inputs:
21
+ wheel-artifact:
22
+ # Empty installs the checkout instead.
23
+ description: Name of the artifact that holds the wheel to install
24
+ type: string
25
+ default: ''
26
+
27
+ concurrency:
28
+ group: smoke-${{ github.workflow }}-${{ github.ref }}
29
+ cancel-in-progress: true
30
+
31
+ jobs:
32
+ install:
33
+ # `uv tool install` reads no lock file, so a dependency upgrade breaks a
34
+ # user here first. DOCX converts in a second without a GPU or models.
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ os: [ubuntu-24.04, ubuntu-24.04-arm, windows-2025]
39
+ runs-on: ${{ matrix.os }}
40
+ timeout-minutes: 20
41
+ defaults:
42
+ run:
43
+ shell: bash
44
+ steps:
45
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
46
+
47
+ - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
48
+ with:
49
+ enable-cache: true
50
+ python-version: "3.12"
51
+
52
+ - name: Ensure pandoc
53
+ # The docx target needs pandoc 3.1.9+; the Ubuntu 24.04 package is
54
+ # 3.1.3, so a pinned release is installed instead.
55
+ env:
56
+ PANDOC_VERSION: 3.10.1
57
+ run: |
58
+ case "$RUNNER_OS" in
59
+ Linux)
60
+ case "$RUNNER_ARCH" in
61
+ X64) arch=amd64 ;;
62
+ ARM64) arch=arm64 ;;
63
+ *) echo "no pandoc release asset for $RUNNER_ARCH" >&2; exit 1 ;;
64
+ esac
65
+ curl -fsSL -o pandoc.deb \
66
+ "https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-${arch}.deb"
67
+ sudo dpkg -i pandoc.deb || (sudo apt-get update -qq && sudo apt-get install -f -y)
68
+ rm pandoc.deb
69
+ ;;
70
+ Windows)
71
+ choco install pandoc -y --no-progress --version="$PANDOC_VERSION"
72
+ echo "C:/Program Files/Pandoc" >> "$GITHUB_PATH"
73
+ ;;
74
+ *) echo "no pandoc recipe for $RUNNER_OS" >&2; exit 1 ;;
75
+ esac
76
+ pandoc --version | head -1
77
+
78
+ - if: inputs.wheel-artifact
79
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
80
+ with:
81
+ name: ${{ inputs.wheel-artifact }}
82
+ path: dist
83
+
84
+ - name: Install raw2md the way a user does
85
+ # The project pins the CUDA build of torch on x64, and the pin wins
86
+ # over --torch-backend; --no-sources drops it, as a wheel install has
87
+ # no pin either.
88
+ env:
89
+ WHEEL_ARTIFACT: ${{ inputs.wheel-artifact }}
90
+ run: |
91
+ if [ -n "$WHEEL_ARTIFACT" ]; then
92
+ uv tool install dist/*.whl --torch-backend cpu
93
+ else
94
+ uv tool install . --no-sources --torch-backend cpu
95
+ fi
96
+ echo "$(uv tool dir --bin)" >> "$GITHUB_PATH"
97
+
98
+ - run: raw2md --version
99
+
100
+ - name: Report readiness
101
+ run: |
102
+ set +e
103
+ raw2md doctor
104
+ code=$?
105
+ set -e
106
+ # 3 is expected on a bare runner (no djvu chain, no API key); any
107
+ # other nonzero code is a real failure.
108
+ if [ "$code" != "0" ] && [ "$code" != "3" ]; then
109
+ exit "$code"
110
+ fi
111
+
112
+ - name: Convert a DOCX end to end
113
+ run: |
114
+ uv run --no-project --with pymupdf python tests/synthesis/gen_smoke.py docx smoke.docx
115
+ raw2md smoke.docx --output-dir out
116
+
117
+ - name: Check the result
118
+ run: bash .github/scripts/check-smoke-result.sh out/smoke.md
119
+
120
+ pdf:
121
+ # marker on a CPU runner, one platform only: gigabytes of models and
122
+ # minutes of inference. Proves the required engine runs without a GPU.
123
+ runs-on: ubuntu-24.04
124
+ timeout-minutes: 45
125
+ defaults:
126
+ run:
127
+ shell: bash
128
+ steps:
129
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
130
+
131
+ - uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
132
+ with:
133
+ enable-cache: true
134
+ python-version: "3.12"
135
+
136
+ # marker's model cache; bump the key suffix to refresh it.
137
+ - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
138
+ with:
139
+ path: ~/.cache/datalab
140
+ key: datalab-models-${{ runner.os }}-v1
141
+
142
+ - if: inputs.wheel-artifact
143
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
144
+ with:
145
+ name: ${{ inputs.wheel-artifact }}
146
+ path: dist
147
+
148
+ - name: Install raw2md the way a user does
149
+ # The project pins the CUDA build of torch on x64, and the pin wins
150
+ # over --torch-backend; --no-sources drops it, as a wheel install has
151
+ # no pin either.
152
+ env:
153
+ WHEEL_ARTIFACT: ${{ inputs.wheel-artifact }}
154
+ run: |
155
+ if [ -n "$WHEEL_ARTIFACT" ]; then
156
+ uv tool install dist/*.whl --torch-backend cpu
157
+ else
158
+ uv tool install . --no-sources --torch-backend cpu
159
+ fi
160
+ echo "$(uv tool dir --bin)" >> "$GITHUB_PATH"
161
+
162
+ - name: Convert a one-page PDF without a GPU
163
+ run: |
164
+ uv run --no-project --with pymupdf python tests/synthesis/gen_smoke.py pdf smoke.pdf
165
+ raw2md smoke.pdf --cuda off --output-dir out
166
+
167
+ - name: Check the result
168
+ run: bash .github/scripts/check-smoke-result.sh out/smoke.md
169
+
170
+ # The stage counters live only in the run log.
171
+ - if: always()
172
+ run: cp ~/.raw2md/logs/raw2md.log ./pdf-smoke.log || true
173
+
174
+ - if: always()
175
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
176
+ with:
177
+ name: pdf-smoke-log
178
+ path: pdf-smoke.log
179
+ if-no-files-found: warn
@@ -0,0 +1,37 @@
1
+ # OS
2
+ .DS_Store
3
+ Thumbs.db
4
+ desktop.ini
5
+
6
+ # Editors
7
+ .vscode/
8
+ .idea/
9
+ *.swp
10
+ *~
11
+
12
+ # Temp
13
+ *.tmp
14
+ *.log
15
+
16
+ # Node (markdownlint tooling)
17
+ node_modules/
18
+
19
+ # Python
20
+ __pycache__/
21
+ *.pyc
22
+ .venv/
23
+ *.egg-info/
24
+ .coverage
25
+ .coverage.*
26
+ coverage.xml
27
+ htmlcov/
28
+ build/
29
+ dist/
30
+
31
+ # Round-trip generated formats (reproducible from fixtures + seed, never committed)
32
+ tests/synthesis/generated/
33
+
34
+ # Default download, run, and scorer folders of the benchmark runner
35
+ mpdocbench-data/
36
+ mpdocbench-run/
37
+ mpdocbench-scorer/
@@ -0,0 +1,31 @@
1
+ {
2
+ // Markdownlint config for raw2md docs.
3
+ "config": {
4
+ "default": true,
5
+ // Line length is enforced by review, not the linter: a path, a command,
6
+ // and a table need a long line.
7
+ "MD013": false,
8
+ // Allow repeated headings as long as they are not siblings.
9
+ "MD024": { "siblings_only": true },
10
+ // Files may start with a comment or frontmatter rather than a heading.
11
+ "MD041": false,
12
+ // The README centers the logo and the badges, and Markdown cannot center:
13
+ // only these tags, with `align`, render on both GitHub and PyPI.
14
+ "MD033": { "allowed_elements": ["h1", "p", "img", "a"] },
15
+ // Table pipe spacing is a free formatting choice (the test corpus uses
16
+ // compact tables on purpose; table reformatting is not a defect).
17
+ "MD060": false
18
+ },
19
+ "globs": ["**/*.md"],
20
+ "ignores": [
21
+ "node_modules/**",
22
+ "**/.venv/**",
23
+ ".git/**",
24
+ ".pytest_cache/**",
25
+ "tests/synthesis/generated/**",
26
+ // Conversion results shown as they came out of raw2md, not authored text.
27
+ "examples/book.md",
28
+ "examples/thesis.md",
29
+ "examples/report.md"
30
+ ]
31
+ }