pagespeak 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 (211) hide show
  1. pagespeak-0.1.0/LICENSE +21 -0
  2. pagespeak-0.1.0/PKG-INFO +235 -0
  3. pagespeak-0.1.0/README.md +180 -0
  4. pagespeak-0.1.0/pyproject.toml +126 -0
  5. pagespeak-0.1.0/setup.cfg +4 -0
  6. pagespeak-0.1.0/src/pagespeak/__init__.py +68 -0
  7. pagespeak-0.1.0/src/pagespeak/_agent_config.py +140 -0
  8. pagespeak-0.1.0/src/pagespeak/_agent_runtime.py +426 -0
  9. pagespeak-0.1.0/src/pagespeak/_db.py +122 -0
  10. pagespeak-0.1.0/src/pagespeak/backends/__init__.py +0 -0
  11. pagespeak-0.1.0/src/pagespeak/backends/_docx.py +190 -0
  12. pagespeak-0.1.0/src/pagespeak/backends/_docx_dispatch.py +66 -0
  13. pagespeak-0.1.0/src/pagespeak/backends/_docx_quality.py +116 -0
  14. pagespeak-0.1.0/src/pagespeak/backends/_docx_structured.py +485 -0
  15. pagespeak-0.1.0/src/pagespeak/backends/_docx_table.py +52 -0
  16. pagespeak-0.1.0/src/pagespeak/backends/_docx_walk.py +131 -0
  17. pagespeak-0.1.0/src/pagespeak/backends/_markdown.py +43 -0
  18. pagespeak-0.1.0/src/pagespeak/backends/_pdf.py +155 -0
  19. pagespeak-0.1.0/src/pagespeak/backends/_pdf_dispatch.py +109 -0
  20. pagespeak-0.1.0/src/pagespeak/backends/_pdf_docling.py +254 -0
  21. pagespeak-0.1.0/src/pagespeak/backends/_qti.py +298 -0
  22. pagespeak-0.1.0/src/pagespeak/backends/_qti_parse.py +217 -0
  23. pagespeak-0.1.0/src/pagespeak/backends/_qti_render.py +154 -0
  24. pagespeak-0.1.0/src/pagespeak/backends/_qti_split.py +187 -0
  25. pagespeak-0.1.0/src/pagespeak/backends/_remote_images.py +312 -0
  26. pagespeak-0.1.0/src/pagespeak/backends/_tophat.py +157 -0
  27. pagespeak-0.1.0/src/pagespeak/backends/_tophat_answers.py +131 -0
  28. pagespeak-0.1.0/src/pagespeak/backends/_tophat_images.py +127 -0
  29. pagespeak-0.1.0/src/pagespeak/backends/_tophat_parse.py +332 -0
  30. pagespeak-0.1.0/src/pagespeak/backends/_tophat_render.py +70 -0
  31. pagespeak-0.1.0/src/pagespeak/cli/__init__.py +166 -0
  32. pagespeak-0.1.0/src/pagespeak/cli/_audit.py +43 -0
  33. pagespeak-0.1.0/src/pagespeak/cli/_baseline.py +19 -0
  34. pagespeak-0.1.0/src/pagespeak/cli/_convert.py +433 -0
  35. pagespeak-0.1.0/src/pagespeak/cli/_deliver.py +62 -0
  36. pagespeak-0.1.0/src/pagespeak/cli/_ingest.py +103 -0
  37. pagespeak-0.1.0/src/pagespeak/cli/_invalidate.py +22 -0
  38. pagespeak-0.1.0/src/pagespeak/cli/_repair.py +125 -0
  39. pagespeak-0.1.0/src/pagespeak/models/__init__.py +0 -0
  40. pagespeak-0.1.0/src/pagespeak/models/_models.py +68 -0
  41. pagespeak-0.1.0/src/pagespeak/models/_pipeline.py +272 -0
  42. pagespeak-0.1.0/src/pagespeak/models/_quiz.py +81 -0
  43. pagespeak-0.1.0/src/pagespeak/orchestrators/__init__.py +0 -0
  44. pagespeak-0.1.0/src/pagespeak/orchestrators/_chunk.py +358 -0
  45. pagespeak-0.1.0/src/pagespeak/orchestrators/_context.py +111 -0
  46. pagespeak-0.1.0/src/pagespeak/orchestrators/_dispatch.py +498 -0
  47. pagespeak-0.1.0/src/pagespeak/orchestrators/_dispatch_setup.py +153 -0
  48. pagespeak-0.1.0/src/pagespeak/orchestrators/_ingest.py +376 -0
  49. pagespeak-0.1.0/src/pagespeak/orchestrators/_phase.py +41 -0
  50. pagespeak-0.1.0/src/pagespeak/orchestrators/_phases.py +438 -0
  51. pagespeak-0.1.0/src/pagespeak/orchestrators/_qti_export.py +127 -0
  52. pagespeak-0.1.0/src/pagespeak/orchestrators/_resume.py +103 -0
  53. pagespeak-0.1.0/src/pagespeak/orchestrators/_sequencer.py +93 -0
  54. pagespeak-0.1.0/src/pagespeak/orchestrators/_split_output.py +123 -0
  55. pagespeak-0.1.0/src/pagespeak/prompts/__init__.py +7 -0
  56. pagespeak-0.1.0/src/pagespeak/prompts/_diagram.py +21 -0
  57. pagespeak-0.1.0/src/pagespeak/prompts/_heading_normalize.py +45 -0
  58. pagespeak-0.1.0/src/pagespeak/prompts/_heading_normalize_full.py +44 -0
  59. pagespeak-0.1.0/src/pagespeak/prompts/_loader.py +60 -0
  60. pagespeak-0.1.0/src/pagespeak/prompts/diagram.yaml +156 -0
  61. pagespeak-0.1.0/src/pagespeak/prompts/heading_normalize.yaml +55 -0
  62. pagespeak-0.1.0/src/pagespeak/prompts/heading_normalize_full.yaml +118 -0
  63. pagespeak-0.1.0/src/pagespeak/py.typed +0 -0
  64. pagespeak-0.1.0/src/pagespeak/services/__init__.py +0 -0
  65. pagespeak-0.1.0/src/pagespeak/services/_audit.py +191 -0
  66. pagespeak-0.1.0/src/pagespeak/services/_audit_checks.py +226 -0
  67. pagespeak-0.1.0/src/pagespeak/services/_baseline.py +112 -0
  68. pagespeak-0.1.0/src/pagespeak/services/_baseline_diff.py +53 -0
  69. pagespeak-0.1.0/src/pagespeak/services/_chunk_rewrite.py +81 -0
  70. pagespeak-0.1.0/src/pagespeak/services/_cleanup.py +377 -0
  71. pagespeak-0.1.0/src/pagespeak/services/_cleanup_diagnose.py +283 -0
  72. pagespeak-0.1.0/src/pagespeak/services/_cleanup_regexes.py +64 -0
  73. pagespeak-0.1.0/src/pagespeak/services/_cleanup_structure.py +346 -0
  74. pagespeak-0.1.0/src/pagespeak/services/_cleanup_transforms.py +314 -0
  75. pagespeak-0.1.0/src/pagespeak/services/_decorations.py +80 -0
  76. pagespeak-0.1.0/src/pagespeak/services/_deliver.py +123 -0
  77. pagespeak-0.1.0/src/pagespeak/services/_diagrams.py +447 -0
  78. pagespeak-0.1.0/src/pagespeak/services/_flat_source_demote.py +110 -0
  79. pagespeak-0.1.0/src/pagespeak/services/_fragments.py +167 -0
  80. pagespeak-0.1.0/src/pagespeak/services/_frontmatter.py +65 -0
  81. pagespeak-0.1.0/src/pagespeak/services/_h1_ratio_rebalance.py +136 -0
  82. pagespeak-0.1.0/src/pagespeak/services/_heading_normalize.py +418 -0
  83. pagespeak-0.1.0/src/pagespeak/services/_heading_sanity.py +242 -0
  84. pagespeak-0.1.0/src/pagespeak/services/_language.py +264 -0
  85. pagespeak-0.1.0/src/pagespeak/services/_listish_headings.py +117 -0
  86. pagespeak-0.1.0/src/pagespeak/services/_normalize_decision.py +200 -0
  87. pagespeak-0.1.0/src/pagespeak/services/_normalize_heuristic.py +129 -0
  88. pagespeak-0.1.0/src/pagespeak/services/_normalize_llm.py +316 -0
  89. pagespeak-0.1.0/src/pagespeak/services/_normalize_repair.py +237 -0
  90. pagespeak-0.1.0/src/pagespeak/services/_outline.py +154 -0
  91. pagespeak-0.1.0/src/pagespeak/services/_presets.py +138 -0
  92. pagespeak-0.1.0/src/pagespeak/services/_provenance.py +96 -0
  93. pagespeak-0.1.0/src/pagespeak/services/_rerun.py +142 -0
  94. pagespeak-0.1.0/src/pagespeak/services/_run_record.py +139 -0
  95. pagespeak-0.1.0/src/pagespeak/services/_split.py +277 -0
  96. pagespeak-0.1.0/src/pagespeak/services/_split_filter.py +276 -0
  97. pagespeak-0.1.0/src/pagespeak/services/_split_parse.py +370 -0
  98. pagespeak-0.1.0/src/pagespeak/services/_split_write.py +485 -0
  99. pagespeak-0.1.0/src/pagespeak/services/_table_repair.py +215 -0
  100. pagespeak-0.1.0/src/pagespeak/services/_toc.py +65 -0
  101. pagespeak-0.1.0/src/pagespeak/services/_vision_backends.py +486 -0
  102. pagespeak-0.1.0/src/pagespeak/services/_vision_cache.py +104 -0
  103. pagespeak-0.1.0/src/pagespeak/services/_vision_parse.py +61 -0
  104. pagespeak-0.1.0/src/pagespeak/utils/__init__.py +0 -0
  105. pagespeak-0.1.0/src/pagespeak/utils/_html.py +128 -0
  106. pagespeak-0.1.0/src/pagespeak/utils/_phash.py +24 -0
  107. pagespeak-0.1.0/src/pagespeak/utils/_prompts.py +14 -0
  108. pagespeak-0.1.0/src/pagespeak/web/__init__.py +81 -0
  109. pagespeak-0.1.0/src/pagespeak/web/__main__.py +17 -0
  110. pagespeak-0.1.0/src/pagespeak/web/_command.py +82 -0
  111. pagespeak-0.1.0/src/pagespeak/web/_config.py +46 -0
  112. pagespeak-0.1.0/src/pagespeak/web/_cost.py +161 -0
  113. pagespeak-0.1.0/src/pagespeak/web/_db.py +16 -0
  114. pagespeak-0.1.0/src/pagespeak/web/_jobs.py +73 -0
  115. pagespeak-0.1.0/src/pagespeak/web/_scan.py +172 -0
  116. pagespeak-0.1.0/src/pagespeak/web/_worker.py +134 -0
  117. pagespeak-0.1.0/src/pagespeak/web/api/__init__.py +1 -0
  118. pagespeak-0.1.0/src/pagespeak/web/api/actions.py +191 -0
  119. pagespeak-0.1.0/src/pagespeak/web/api/pages.py +141 -0
  120. pagespeak-0.1.0/src/pagespeak/web/api/partials.py +168 -0
  121. pagespeak-0.1.0/src/pagespeak.egg-info/PKG-INFO +235 -0
  122. pagespeak-0.1.0/src/pagespeak.egg-info/SOURCES.txt +209 -0
  123. pagespeak-0.1.0/src/pagespeak.egg-info/dependency_links.txt +1 -0
  124. pagespeak-0.1.0/src/pagespeak.egg-info/entry_points.txt +2 -0
  125. pagespeak-0.1.0/src/pagespeak.egg-info/requires.txt +38 -0
  126. pagespeak-0.1.0/src/pagespeak.egg-info/top_level.txt +1 -0
  127. pagespeak-0.1.0/tests/test_agent_runtime.py +849 -0
  128. pagespeak-0.1.0/tests/test_agent_runtime_job_id.py +100 -0
  129. pagespeak-0.1.0/tests/test_audit.py +192 -0
  130. pagespeak-0.1.0/tests/test_audit_checks.py +259 -0
  131. pagespeak-0.1.0/tests/test_baseline.py +104 -0
  132. pagespeak-0.1.0/tests/test_baseline_diff.py +86 -0
  133. pagespeak-0.1.0/tests/test_chunk.py +534 -0
  134. pagespeak-0.1.0/tests/test_chunk_rewrite.py +94 -0
  135. pagespeak-0.1.0/tests/test_cleanup.py +1213 -0
  136. pagespeak-0.1.0/tests/test_cleanup_diagnose.py +319 -0
  137. pagespeak-0.1.0/tests/test_cli.py +343 -0
  138. pagespeak-0.1.0/tests/test_cli_audit.py +64 -0
  139. pagespeak-0.1.0/tests/test_cli_baseline.py +163 -0
  140. pagespeak-0.1.0/tests/test_cli_convert.py +135 -0
  141. pagespeak-0.1.0/tests/test_cli_ingest.py +166 -0
  142. pagespeak-0.1.0/tests/test_cli_invalidate.py +80 -0
  143. pagespeak-0.1.0/tests/test_cli_repair.py +109 -0
  144. pagespeak-0.1.0/tests/test_context.py +83 -0
  145. pagespeak-0.1.0/tests/test_db.py +186 -0
  146. pagespeak-0.1.0/tests/test_decorations.py +68 -0
  147. pagespeak-0.1.0/tests/test_deliver.py +127 -0
  148. pagespeak-0.1.0/tests/test_diagrams.py +1295 -0
  149. pagespeak-0.1.0/tests/test_dispatch.py +1316 -0
  150. pagespeak-0.1.0/tests/test_docx.py +164 -0
  151. pagespeak-0.1.0/tests/test_docx_dispatch.py +30 -0
  152. pagespeak-0.1.0/tests/test_docx_quality.py +150 -0
  153. pagespeak-0.1.0/tests/test_docx_structured.py +712 -0
  154. pagespeak-0.1.0/tests/test_docx_table.py +105 -0
  155. pagespeak-0.1.0/tests/test_docx_walk.py +56 -0
  156. pagespeak-0.1.0/tests/test_flat_source_demote.py +150 -0
  157. pagespeak-0.1.0/tests/test_fragments.py +174 -0
  158. pagespeak-0.1.0/tests/test_frontmatter.py +76 -0
  159. pagespeak-0.1.0/tests/test_h1_ratio_rebalance.py +231 -0
  160. pagespeak-0.1.0/tests/test_heading_normalize.py +1091 -0
  161. pagespeak-0.1.0/tests/test_heading_sanity.py +339 -0
  162. pagespeak-0.1.0/tests/test_html.py +166 -0
  163. pagespeak-0.1.0/tests/test_ingest.py +274 -0
  164. pagespeak-0.1.0/tests/test_init.py +37 -0
  165. pagespeak-0.1.0/tests/test_language.py +85 -0
  166. pagespeak-0.1.0/tests/test_listish_headings.py +97 -0
  167. pagespeak-0.1.0/tests/test_markdown.py +58 -0
  168. pagespeak-0.1.0/tests/test_models.py +22 -0
  169. pagespeak-0.1.0/tests/test_normalize_decision.py +130 -0
  170. pagespeak-0.1.0/tests/test_normalize_repair.py +267 -0
  171. pagespeak-0.1.0/tests/test_outline.py +283 -0
  172. pagespeak-0.1.0/tests/test_pdf.py +235 -0
  173. pagespeak-0.1.0/tests/test_pdf_dispatch.py +147 -0
  174. pagespeak-0.1.0/tests/test_pdf_docling.py +267 -0
  175. pagespeak-0.1.0/tests/test_phase.py +30 -0
  176. pagespeak-0.1.0/tests/test_phases.py +212 -0
  177. pagespeak-0.1.0/tests/test_pipeline.py +225 -0
  178. pagespeak-0.1.0/tests/test_presets.py +110 -0
  179. pagespeak-0.1.0/tests/test_prompt_diagram.py +174 -0
  180. pagespeak-0.1.0/tests/test_prompt_heading_normalize.py +48 -0
  181. pagespeak-0.1.0/tests/test_prompts_loader.py +88 -0
  182. pagespeak-0.1.0/tests/test_provenance.py +94 -0
  183. pagespeak-0.1.0/tests/test_qti.py +264 -0
  184. pagespeak-0.1.0/tests/test_qti_parse.py +307 -0
  185. pagespeak-0.1.0/tests/test_qti_render.py +165 -0
  186. pagespeak-0.1.0/tests/test_qti_split.py +135 -0
  187. pagespeak-0.1.0/tests/test_quiz_models.py +64 -0
  188. pagespeak-0.1.0/tests/test_remote_images.py +498 -0
  189. pagespeak-0.1.0/tests/test_rerun.py +294 -0
  190. pagespeak-0.1.0/tests/test_resume.py +274 -0
  191. pagespeak-0.1.0/tests/test_run_record.py +216 -0
  192. pagespeak-0.1.0/tests/test_sequencer.py +113 -0
  193. pagespeak-0.1.0/tests/test_split.py +1638 -0
  194. pagespeak-0.1.0/tests/test_split_filter.py +90 -0
  195. pagespeak-0.1.0/tests/test_table_repair.py +167 -0
  196. pagespeak-0.1.0/tests/test_toc.py +85 -0
  197. pagespeak-0.1.0/tests/test_tophat.py +428 -0
  198. pagespeak-0.1.0/tests/test_tophat_answers.py +53 -0
  199. pagespeak-0.1.0/tests/test_tophat_images.py +49 -0
  200. pagespeak-0.1.0/tests/test_vision_cache.py +113 -0
  201. pagespeak-0.1.0/tests/test_web_actions.py +152 -0
  202. pagespeak-0.1.0/tests/test_web_app.py +38 -0
  203. pagespeak-0.1.0/tests/test_web_command.py +62 -0
  204. pagespeak-0.1.0/tests/test_web_config.py +45 -0
  205. pagespeak-0.1.0/tests/test_web_cost.py +65 -0
  206. pagespeak-0.1.0/tests/test_web_db.py +27 -0
  207. pagespeak-0.1.0/tests/test_web_jobs.py +34 -0
  208. pagespeak-0.1.0/tests/test_web_llm_summary.py +53 -0
  209. pagespeak-0.1.0/tests/test_web_pages.py +260 -0
  210. pagespeak-0.1.0/tests/test_web_scan.py +80 -0
  211. pagespeak-0.1.0/tests/test_web_worker.py +100 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mike Farr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.4
2
+ Name: pagespeak
3
+ Version: 0.1.0
4
+ Summary: Convert PDFs and Office documents to LLM-friendly markdown, with diagram extraction to Mermaid.
5
+ Author: Mike Farr
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/phierceweb/pagespeak
8
+ Project-URL: Repository, https://github.com/phierceweb/pagespeak
9
+ Project-URL: Changelog, https://github.com/phierceweb/pagespeak/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/phierceweb/pagespeak/issues
11
+ Keywords: pdf,docx,markdown,llm,rag,ingest,mermaid
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Text Processing :: Markup :: Markdown
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: anthropic>=0.39.0
24
+ Requires-Dist: beautifulsoup4>=4.12
25
+ Requires-Dist: httpx>=0.25
26
+ Requires-Dist: markdownify>=0.13
27
+ Requires-Dist: markitdown[docx,outlook,pptx,xlsx]>=0.0.1a3
28
+ Requires-Dist: pf-core[image-phash,llm,tracking]~=0.2.0
29
+ Requires-Dist: typer>=0.12.0
30
+ Provides-Extra: pdf
31
+ Requires-Dist: marker-pdf>=0.2.0; extra == "pdf"
32
+ Requires-Dist: pypdfium2; extra == "pdf"
33
+ Provides-Extra: pdf-docling
34
+ Requires-Dist: docling>=2.0; extra == "pdf-docling"
35
+ Requires-Dist: pypdfium2; extra == "pdf-docling"
36
+ Provides-Extra: docx-structured
37
+ Requires-Dist: python-docx>=1.1; extra == "docx-structured"
38
+ Provides-Extra: tophat
39
+ Requires-Dist: pypdfium2; extra == "tophat"
40
+ Provides-Extra: web
41
+ Requires-Dist: fastapi>=0.110; extra == "web"
42
+ Requires-Dist: uvicorn[standard]>=0.29; extra == "web"
43
+ Requires-Dist: jinja2>=3.1; extra == "web"
44
+ Requires-Dist: python-multipart>=0.0.9; extra == "web"
45
+ Provides-Extra: postgres
46
+ Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=8.0; extra == "dev"
49
+ Requires-Dist: pytest-mock>=3.12; extra == "dev"
50
+ Requires-Dist: ruff>=0.5; extra == "dev"
51
+ Requires-Dist: mypy>=1.10; extra == "dev"
52
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
53
+ Requires-Dist: pre-commit>=3.7; extra == "dev"
54
+ Dynamic: license-file
55
+
56
+ # pagespeak
57
+
58
+ Convert PDF, Word, and other office formats to LLM-friendly markdown — with diagram extraction to Mermaid.
59
+
60
+ ## Why
61
+
62
+ pagespeak began as the ingestion step for a retrieval system built from a large, diverse body of real documents — equipment and software manuals, textbooks, how-to guides, and HTML help sites. Every off-the-shelf converter produced markdown that looked clean and retrieved badly: heading hierarchy flattened so it couldn't be split into coherent chunks, wide tables collapsed into one cell, diagrams reduced to image refs with no text, HTML entities left undecoded. The extraction looked finished; the output wasn't usable.
63
+
64
+ Nothing in the mainstream stack closes that gap — the usual answer is to ignore structure and split on a fixed token window. So pagespeak became the layer that does. It delegates extraction to Marker, Docling, or MarkItDown and adds the passes that prepare the output for LLM/RAG use — each one a fix for a specific way a real document broke, found by converting it and reading the output, one corpus defect at a time:
65
+
66
+ - **Diagrams → Mermaid, illustrations → labeled captions.** Extractors leave an embedded image with no text for retrieval to match. pagespeak sends each to a vision model: *structural* graphics (flowcharts, signal flow, state and sequence diagrams) become a tagged Mermaid block an LLM can read and edit; *morphological* figures where the picture itself is the information (anatomical illustrations, micrographs, charts) instead get a dense caption that transcribes the visible labels. The original image is always kept beside it.
67
+ - **Heading repair, then section split.** It renormalizes flattened heading levels and splits the document into per-section files with in-text breadcrumbs, so each chunk carries its parent context.
68
+ - **Decoration stripping, content-keyed caching, cost controls, and an output audit.** `pagespeak audit` scans converted markdown for the defects above and reports them; it runs on any markdown tree, not just pagespeak's.
69
+
70
+ Extraction stays delegated — pagespeak does not try to out-parse Marker or Docling. Wide tables are the main weak spot: when Marker collapses a multi-column spec table into a single cell, `pagespeak repair-tables` re-reads just that page with Docling and splices the clean grid back into the Marker output, rather than re-converting the whole document.
71
+
72
+ That structure is the payoff. It lets a *compound, cross-document* question — one needing the right sections from several manuals at once — be answered from a few thousand relevant tokens, every step traceable to the manual it came from, instead of from documents too large to load.
73
+
74
+ It is new as a *public* project, not as code: these passes were hardened one real document at a time — convert, read the output, fix what broke — against a large working corpus well before this release. The worked examples below are the receipts.
75
+
76
+ > **See it on real documents — [docs/worked-examples.md](docs/worked-examples.md).** One 18-page textbook chapter run through raw Marker, raw Docling, and pagespeak side by side (the heading-repair before/after, a real diagram→Mermaid, and a retrieval query the figure answers but the prose can't), then a 68-manual / ~6.1M-token library where one compound question is answered from ~1,600 retrieved tokens across three manuals — with the dated models and the honest misses reported, not hidden. It's the empirical case for everything above, and the best way to judge whether this fits your corpus.
77
+
78
+ ## Scope — where pagespeak fits
79
+
80
+ pagespeak is the **ingestion and structuring** stage of a retrieval pipeline, not a whole one. It converts documents to clean, per-section markdown with breadcrumbs and provenance — and stops there. It does **not** do embeddings, vector storage, retrieval, or a query/chat layer; pair it with your own vector DB and retrieval framework (LlamaIndex, LangChain, Haystack, or hand-rolled).
81
+
82
+ One thing to know going in: the section split is **structural, not size-based**. Sections are cut at heading boundaries — there is no token budget, no max-chunk size, and no overlap. A long section stays one file; a near-empty one is dropped (`min_body_chars`). That makes each file a coherent, self-locating unit — which is what you want feeding an embedder — but if your retrieval needs uniformly-sized chunks, add a token-aware splitter downstream. The structure pagespeak recovers (correct heading levels, in-text breadcrumbs) is exactly what makes that downstream chunking clean.
83
+
84
+ ## Install
85
+
86
+ > **Pre-PyPI:** the first PyPI release is pending, so `pip install pagespeak` is not live yet. Until it lands, install from source — `pip install "pagespeak @ git+https://github.com/phierceweb/pagespeak"`, adding extras as `"pagespeak[pdf] @ git+https://github.com/phierceweb/pagespeak"`. Once published, the commands below work as written.
87
+
88
+ ```bash
89
+ pip install pagespeak # DOCX/PPTX/XLSX/HTML/CSV/JSON/...
90
+ pip install pagespeak[pdf] # adds Marker for PDF (default)
91
+ pip install pagespeak[pdf-docling] # adds Docling for PDF (accuracy-first)
92
+ pip install pagespeak[pdf,pdf-docling] # both — pick at call time
93
+ pip install pagespeak[docx-structured] # adds python-docx for structure-faithful DOCX
94
+ pip install pagespeak[pdf,docx-structured] # PDF + structure-faithful DOCX
95
+ pip install pagespeak[tophat] # adds the Top Hat quiz-export backend (light; pypdfium2)
96
+ pip install pagespeak[web] # localhost web console (FastAPI + uvicorn)
97
+ ```
98
+
99
+ Pagespeak builds on [`pf-core`](https://github.com/phierceweb/pf-core) for its LLM clients (Anthropic / Claude Code / OpenRouter), structured logging, pipeline manifest helpers, CLI subcommand factories, and atomic-write utilities. `pf-core[image-phash]` is pulled in transitively — no separate install step required.
100
+
101
+ ## Quickstart
102
+
103
+ ```python
104
+ from pagespeak import to_markdown
105
+
106
+ result = to_markdown("manual.pdf", output_dir="./out", diagrams=True)
107
+
108
+ result.markdown # final markdown with mermaid blocks embedded
109
+ result.images # list[Path] of extracted images
110
+ result.diagrams # list[Diagram(image_path, caption, mermaid, diagram_type)]
111
+ ```
112
+
113
+ ```bash
114
+ # One command: ingest + Phase 3 (cleanup, normalize, vision, split)
115
+ pagespeak convert manual.pdf -o ./out
116
+ pagespeak convert report.docx -o ./out --no-diagrams
117
+
118
+ # Two commands: backend phase separately, iterate Phase 3
119
+ pagespeak ingest thick.pdf -o ./out --workers 4 # chunked-parallel Marker
120
+ pagespeak convert ./out --normalize-headings # Phase 3 on existing raw.md
121
+ ```
122
+
123
+ For RAG-shaped output (split into per-section files with sensible defaults):
124
+
125
+ ```bash
126
+ pagespeak convert manual.pdf -o ./out --preset rag-default
127
+ ```
128
+
129
+ `rag-default`'s heading mode is `heuristic`, which is right for cleanly-numbered documents (textbooks, specs with `1.1`/`1.2` sections) — but it skips on **un-numbered manuals**, where it leaves the flattened hierarchy in place. For those (most consumer-electronics / AV / software manuals), add the LLM heading-repair pass — this is the canonical recipe for a real manual corpus:
130
+
131
+ ```bash
132
+ pagespeak convert manual.pdf -o ./out --preset rag-default --normalize-headings-mode llm_full
133
+ ```
134
+
135
+ See [docs/presets.md](docs/presets.md) for the five built-in presets and [docs/choosing-defaults.md](docs/choosing-defaults.md) for the per-document-type triage (when to add `llm_full`, `--device cpu`, page-ranging, and more).
136
+
137
+ ## Output shape
138
+
139
+ For each diagram detected, the caption goes in the image's alt text and a tagged Mermaid block follows:
140
+
141
+ ````markdown
142
+ ![Two-panel figure showing negative feedback control loops. Panel (a) presents the general model: a stimulus activates a sensor, which signals a control center that directs an effector, with a feedback loop returning to the stimulus. Panel (b) demonstrates thermoregulation: elevated body temperature is detected by nerve cells, processed by the brain's temperature regulatory center, and triggers sweating to cool the body.](images/_page_30_Figure_4.jpeg)
143
+
144
+ ```mermaid pagespeak-image="images/_page_30_Figure_4.jpeg"
145
+ flowchart TD
146
+ A["Body temperature exceeds 37°C"]
147
+ B["Nerve cells in skin and brain"]
148
+ C["Temperature regulatory center in brain"]
149
+ D["Sweat glands throughout body"]
150
+ A --> B
151
+ B --> C
152
+ C --> D
153
+ D -.-> A
154
+ ```
155
+ ````
156
+
157
+ - **Captions live in alt text** — extractable without parsing prose, read by screen readers.
158
+ - **Mermaid blocks tag their source image** with `pagespeak-image="<path>"` on the fenced-block info string. Renderers ignore the tag; parsers can pair Mermaid with the image it was generated from.
159
+ - **Non-structural images** — photos, screenshots, and morphological figures (anatomy, micrographs, chemical structures, charts) — get a caption instead of Mermaid; label-bearing illustrations get a caption that transcribes their visible labels.
160
+ - **Repeated decorations** (page headers, footer logos) are detected via perceptual-hash clustering and stripped from the consolidated markdown.
161
+
162
+ > **See it on a real document:** [docs/worked-examples.md](docs/worked-examples.md) runs one chapter of a CC-BY textbook through raw Marker, raw Docling, and pagespeak — the heading-repair before/after, a diagram→Mermaid, and a retrieval query the figure answers but the prose can't.
163
+
164
+ ## Vision backends
165
+
166
+ | Backend | When to use | Auth |
167
+ |---|---|---|
168
+ | `claude_code` (default) | $0/call via a Claude Code subscription | `claude` binary on PATH |
169
+ | `anthropic` | Direct API; fastest | `ANTHROPIC_API_KEY` |
170
+ | `openrouter` | Multi-provider unified billing (Gemini, Llama vision, …) | `OPENROUTER_API_KEY` |
171
+
172
+ The default model is Claude Haiku 4.5 — $0 on the default `claude_code` backend, or typically $0.001–$0.005 per image on a paid backend. See [docs/diagrams.md](docs/diagrams.md) for backend mechanics, prompt versioning, and failure handling.
173
+
174
+ Vision output is best-effort. A diagram's Mermaid is a model's *reading* of the image — usually faithful for clean structural figures, but it can be approximate or wrong on dense, hand-drawn, or low-resolution ones, and a confidently-wrong caption is worse than none. pagespeak keeps the original image beside every block, biases the prompt toward caption-only when a figure isn't cleanly structural, and for critical content you should spot-check the Mermaid against the source rather than trust it blindly. The worked examples report the real per-figure hit rate (e.g. organs named for 7 of 11 body systems), not a perfect one.
175
+
176
+ ## Format support
177
+
178
+ | Format | Backend |
179
+ |---|---|
180
+ | `.pdf` | [Marker](https://github.com/VikParuchuri/marker) (default, fast) or [Docling](https://github.com/DS4SD/docling) (accuracy-first). See [docs/backends.md](docs/backends.md). |
181
+ | `.docx`, `.pptx`, `.xlsx`, `.html`, `.htm`, `.csv`, `.json`, `.xml`, `.epub` | [MarkItDown](https://github.com/microsoft/markitdown) |
182
+ | Canvas QTI quiz export (directory or `.imscc`) | Built-in QTI backend → one markdown file per quiz with the answer key. See [docs/canvas-quizzes.md](docs/canvas-quizzes.md). |
183
+ | Top Hat quiz-export PDF | `--pdf-backend tophat` → one `## Question N` block per question, correct answer marked when revealed, embedded figures extracted + captioned. See [docs/tophat-quizzes.md](docs/tophat-quizzes.md). |
184
+
185
+ ## How it relates to other tools
186
+
187
+ pagespeak is not a parser — it wraps existing extractors and runs cleanup, structuring, and diagram passes around their output.
188
+
189
+ | Tool | What it is | How pagespeak relates |
190
+ |---|---|---|
191
+ | [MarkItDown](https://github.com/microsoft/markitdown), [Marker](https://github.com/VikParuchuri/marker), [Docling](https://github.com/DS4SD/docling) | Open-source document → markdown extractors | Used as pagespeak's backends; pagespeak runs heading repair, section splitting, decoration stripping, and diagram→Mermaid on their output |
192
+ | LlamaParse, Reducto, Mathpix | Hosted, paid extraction APIs for complex documents | Different model — pagespeak runs locally on the extractors above, with optional $0 vision via Claude Code |
193
+ | Unstructured | Partitions documents into typed elements for RAG frameworks | Different output — pagespeak emits per-section markdown files with breadcrumbs and embedded Mermaid |
194
+
195
+ ### Why a layer at all — heading fidelity
196
+
197
+ The hardest part of PDF→markdown for RAG is the heading tree, because that's what chunking splits on. PDFs don't store semantic heading levels — only font sizes — so every extractor *guesses*, and each flattens or mis-levels real documents in a different way:
198
+
199
+ | | Heading hierarchy | Tables | Figures / formulas |
200
+ |---|---|---|---|
201
+ | **Marker** (default) | 4-level pyramid in single-shot; **flattens in the chunked pipeline** (per-chunk font stats disagree). MPS crash on Apple Silicon → `--device cpu` | occasionally collapses a multi-column table into one cell | — |
202
+ | **Docling** | **capped at 2 levels by design** — its layout model labels every section heading `level=1` | well-formed, TableFormer-grade | ~25% more figures on textbooks; formula → LaTeX |
203
+
204
+ So no backend simply gets structure right, and "just use Marker" or "just use Docling" inherits that backend's specific failure. Recovering the structure is pagespeak's reason to exist: an optional LLM heading-renormalization stage rebuilds a flattened hierarchy, deterministic post-passes repair levels at $0, and `repair-tables` re-reads *just* a collapsed table's page through Docling and splices the clean grid back into Marker's output — one page, not a second full conversion. Pick the backend for its strengths; pagespeak patches its known weakness. The full trade-off and recipes: [docs/backends.md](docs/backends.md) and [docs/choosing-defaults.md](docs/choosing-defaults.md).
205
+
206
+ ## Docs
207
+
208
+ - [docs/pipeline.md](docs/pipeline.md) — stage-by-stage walkthrough of what every command runs (spine)
209
+ - [docs/worked-examples.md](docs/worked-examples.md) — end-to-end before/after on real documents: extraction, repair, retrieval, and the cross-document payoff
210
+ - [docs/usage.md](docs/usage.md) — library + CLI examples, kwargs, env vars, common recipes
211
+ - [docs/choosing-defaults.md](docs/choosing-defaults.md) — pre-ingest triage: canonical recipe, vendor patterns, when to deviate
212
+ - [docs/presets.md](docs/presets.md) — config presets and `<output>/.pagespeak-run.json`
213
+ - [docs/architecture.md](docs/architecture.md) — module layout, data flow
214
+ - [docs/diagrams.md](docs/diagrams.md) — vision pass, prompt versioning
215
+ - [docs/cleanup.md](docs/cleanup.md) — cleanup levels, cross-refs, section splitting
216
+ - [docs/normalize-headings.md](docs/normalize-headings.md) — heading-level renormalization
217
+ - [docs/audit.md](docs/audit.md) — `pagespeak audit`: scan converted output for conversion defects (read-only, $0)
218
+ - [docs/repair-tables.md](docs/repair-tables.md) — `pagespeak repair-tables`: splice Docling's clean grid into Marker-collapsed tables (the fix for the audit's `collapsed_table`)
219
+ - [docs/caching.md](docs/caching.md) — cache layers, `--rerun-from`, baselines, diff
220
+ - [docs/backends.md](docs/backends.md) — Marker vs Docling for PDF
221
+ - [docs/docx-backends.md](docs/docx-backends.md) — MarkItDown vs python-docx for DOCX
222
+ - [docs/ingest.md](docs/ingest.md) — `pagespeak ingest`, chunked-parallel workers, resume semantics
223
+ - [docs/format-support.md](docs/format-support.md) — per-format quirks
224
+ - [docs/canvas-quizzes.md](docs/canvas-quizzes.md) — Canvas QTI quiz exports → one markdown file per quiz
225
+ - [docs/tophat-quizzes.md](docs/tophat-quizzes.md) — Top Hat quiz-export PDFs → per-question markdown (`--pdf-backend tophat`)
226
+ - [docs/operations.md](docs/operations.md) — sandbox / `ProcessPoolExecutor` gotchas
227
+ - [docs/web.md](docs/web.md) — web console: upload/queue, per-phase cockpit, cost gate, LLM observability
228
+
229
+ ## Security
230
+
231
+ [SECURITY.md](SECURITY.md) covers vulnerability reporting and safe-usage notes for shared environments (the console has no auth; remote-image fetching is SSRF-guarded).
232
+
233
+ ## License
234
+
235
+ MIT.
@@ -0,0 +1,180 @@
1
+ # pagespeak
2
+
3
+ Convert PDF, Word, and other office formats to LLM-friendly markdown — with diagram extraction to Mermaid.
4
+
5
+ ## Why
6
+
7
+ pagespeak began as the ingestion step for a retrieval system built from a large, diverse body of real documents — equipment and software manuals, textbooks, how-to guides, and HTML help sites. Every off-the-shelf converter produced markdown that looked clean and retrieved badly: heading hierarchy flattened so it couldn't be split into coherent chunks, wide tables collapsed into one cell, diagrams reduced to image refs with no text, HTML entities left undecoded. The extraction looked finished; the output wasn't usable.
8
+
9
+ Nothing in the mainstream stack closes that gap — the usual answer is to ignore structure and split on a fixed token window. So pagespeak became the layer that does. It delegates extraction to Marker, Docling, or MarkItDown and adds the passes that prepare the output for LLM/RAG use — each one a fix for a specific way a real document broke, found by converting it and reading the output, one corpus defect at a time:
10
+
11
+ - **Diagrams → Mermaid, illustrations → labeled captions.** Extractors leave an embedded image with no text for retrieval to match. pagespeak sends each to a vision model: *structural* graphics (flowcharts, signal flow, state and sequence diagrams) become a tagged Mermaid block an LLM can read and edit; *morphological* figures where the picture itself is the information (anatomical illustrations, micrographs, charts) instead get a dense caption that transcribes the visible labels. The original image is always kept beside it.
12
+ - **Heading repair, then section split.** It renormalizes flattened heading levels and splits the document into per-section files with in-text breadcrumbs, so each chunk carries its parent context.
13
+ - **Decoration stripping, content-keyed caching, cost controls, and an output audit.** `pagespeak audit` scans converted markdown for the defects above and reports them; it runs on any markdown tree, not just pagespeak's.
14
+
15
+ Extraction stays delegated — pagespeak does not try to out-parse Marker or Docling. Wide tables are the main weak spot: when Marker collapses a multi-column spec table into a single cell, `pagespeak repair-tables` re-reads just that page with Docling and splices the clean grid back into the Marker output, rather than re-converting the whole document.
16
+
17
+ That structure is the payoff. It lets a *compound, cross-document* question — one needing the right sections from several manuals at once — be answered from a few thousand relevant tokens, every step traceable to the manual it came from, instead of from documents too large to load.
18
+
19
+ It is new as a *public* project, not as code: these passes were hardened one real document at a time — convert, read the output, fix what broke — against a large working corpus well before this release. The worked examples below are the receipts.
20
+
21
+ > **See it on real documents — [docs/worked-examples.md](docs/worked-examples.md).** One 18-page textbook chapter run through raw Marker, raw Docling, and pagespeak side by side (the heading-repair before/after, a real diagram→Mermaid, and a retrieval query the figure answers but the prose can't), then a 68-manual / ~6.1M-token library where one compound question is answered from ~1,600 retrieved tokens across three manuals — with the dated models and the honest misses reported, not hidden. It's the empirical case for everything above, and the best way to judge whether this fits your corpus.
22
+
23
+ ## Scope — where pagespeak fits
24
+
25
+ pagespeak is the **ingestion and structuring** stage of a retrieval pipeline, not a whole one. It converts documents to clean, per-section markdown with breadcrumbs and provenance — and stops there. It does **not** do embeddings, vector storage, retrieval, or a query/chat layer; pair it with your own vector DB and retrieval framework (LlamaIndex, LangChain, Haystack, or hand-rolled).
26
+
27
+ One thing to know going in: the section split is **structural, not size-based**. Sections are cut at heading boundaries — there is no token budget, no max-chunk size, and no overlap. A long section stays one file; a near-empty one is dropped (`min_body_chars`). That makes each file a coherent, self-locating unit — which is what you want feeding an embedder — but if your retrieval needs uniformly-sized chunks, add a token-aware splitter downstream. The structure pagespeak recovers (correct heading levels, in-text breadcrumbs) is exactly what makes that downstream chunking clean.
28
+
29
+ ## Install
30
+
31
+ > **Pre-PyPI:** the first PyPI release is pending, so `pip install pagespeak` is not live yet. Until it lands, install from source — `pip install "pagespeak @ git+https://github.com/phierceweb/pagespeak"`, adding extras as `"pagespeak[pdf] @ git+https://github.com/phierceweb/pagespeak"`. Once published, the commands below work as written.
32
+
33
+ ```bash
34
+ pip install pagespeak # DOCX/PPTX/XLSX/HTML/CSV/JSON/...
35
+ pip install pagespeak[pdf] # adds Marker for PDF (default)
36
+ pip install pagespeak[pdf-docling] # adds Docling for PDF (accuracy-first)
37
+ pip install pagespeak[pdf,pdf-docling] # both — pick at call time
38
+ pip install pagespeak[docx-structured] # adds python-docx for structure-faithful DOCX
39
+ pip install pagespeak[pdf,docx-structured] # PDF + structure-faithful DOCX
40
+ pip install pagespeak[tophat] # adds the Top Hat quiz-export backend (light; pypdfium2)
41
+ pip install pagespeak[web] # localhost web console (FastAPI + uvicorn)
42
+ ```
43
+
44
+ Pagespeak builds on [`pf-core`](https://github.com/phierceweb/pf-core) for its LLM clients (Anthropic / Claude Code / OpenRouter), structured logging, pipeline manifest helpers, CLI subcommand factories, and atomic-write utilities. `pf-core[image-phash]` is pulled in transitively — no separate install step required.
45
+
46
+ ## Quickstart
47
+
48
+ ```python
49
+ from pagespeak import to_markdown
50
+
51
+ result = to_markdown("manual.pdf", output_dir="./out", diagrams=True)
52
+
53
+ result.markdown # final markdown with mermaid blocks embedded
54
+ result.images # list[Path] of extracted images
55
+ result.diagrams # list[Diagram(image_path, caption, mermaid, diagram_type)]
56
+ ```
57
+
58
+ ```bash
59
+ # One command: ingest + Phase 3 (cleanup, normalize, vision, split)
60
+ pagespeak convert manual.pdf -o ./out
61
+ pagespeak convert report.docx -o ./out --no-diagrams
62
+
63
+ # Two commands: backend phase separately, iterate Phase 3
64
+ pagespeak ingest thick.pdf -o ./out --workers 4 # chunked-parallel Marker
65
+ pagespeak convert ./out --normalize-headings # Phase 3 on existing raw.md
66
+ ```
67
+
68
+ For RAG-shaped output (split into per-section files with sensible defaults):
69
+
70
+ ```bash
71
+ pagespeak convert manual.pdf -o ./out --preset rag-default
72
+ ```
73
+
74
+ `rag-default`'s heading mode is `heuristic`, which is right for cleanly-numbered documents (textbooks, specs with `1.1`/`1.2` sections) — but it skips on **un-numbered manuals**, where it leaves the flattened hierarchy in place. For those (most consumer-electronics / AV / software manuals), add the LLM heading-repair pass — this is the canonical recipe for a real manual corpus:
75
+
76
+ ```bash
77
+ pagespeak convert manual.pdf -o ./out --preset rag-default --normalize-headings-mode llm_full
78
+ ```
79
+
80
+ See [docs/presets.md](docs/presets.md) for the five built-in presets and [docs/choosing-defaults.md](docs/choosing-defaults.md) for the per-document-type triage (when to add `llm_full`, `--device cpu`, page-ranging, and more).
81
+
82
+ ## Output shape
83
+
84
+ For each diagram detected, the caption goes in the image's alt text and a tagged Mermaid block follows:
85
+
86
+ ````markdown
87
+ ![Two-panel figure showing negative feedback control loops. Panel (a) presents the general model: a stimulus activates a sensor, which signals a control center that directs an effector, with a feedback loop returning to the stimulus. Panel (b) demonstrates thermoregulation: elevated body temperature is detected by nerve cells, processed by the brain's temperature regulatory center, and triggers sweating to cool the body.](images/_page_30_Figure_4.jpeg)
88
+
89
+ ```mermaid pagespeak-image="images/_page_30_Figure_4.jpeg"
90
+ flowchart TD
91
+ A["Body temperature exceeds 37°C"]
92
+ B["Nerve cells in skin and brain"]
93
+ C["Temperature regulatory center in brain"]
94
+ D["Sweat glands throughout body"]
95
+ A --> B
96
+ B --> C
97
+ C --> D
98
+ D -.-> A
99
+ ```
100
+ ````
101
+
102
+ - **Captions live in alt text** — extractable without parsing prose, read by screen readers.
103
+ - **Mermaid blocks tag their source image** with `pagespeak-image="<path>"` on the fenced-block info string. Renderers ignore the tag; parsers can pair Mermaid with the image it was generated from.
104
+ - **Non-structural images** — photos, screenshots, and morphological figures (anatomy, micrographs, chemical structures, charts) — get a caption instead of Mermaid; label-bearing illustrations get a caption that transcribes their visible labels.
105
+ - **Repeated decorations** (page headers, footer logos) are detected via perceptual-hash clustering and stripped from the consolidated markdown.
106
+
107
+ > **See it on a real document:** [docs/worked-examples.md](docs/worked-examples.md) runs one chapter of a CC-BY textbook through raw Marker, raw Docling, and pagespeak — the heading-repair before/after, a diagram→Mermaid, and a retrieval query the figure answers but the prose can't.
108
+
109
+ ## Vision backends
110
+
111
+ | Backend | When to use | Auth |
112
+ |---|---|---|
113
+ | `claude_code` (default) | $0/call via a Claude Code subscription | `claude` binary on PATH |
114
+ | `anthropic` | Direct API; fastest | `ANTHROPIC_API_KEY` |
115
+ | `openrouter` | Multi-provider unified billing (Gemini, Llama vision, …) | `OPENROUTER_API_KEY` |
116
+
117
+ The default model is Claude Haiku 4.5 — $0 on the default `claude_code` backend, or typically $0.001–$0.005 per image on a paid backend. See [docs/diagrams.md](docs/diagrams.md) for backend mechanics, prompt versioning, and failure handling.
118
+
119
+ Vision output is best-effort. A diagram's Mermaid is a model's *reading* of the image — usually faithful for clean structural figures, but it can be approximate or wrong on dense, hand-drawn, or low-resolution ones, and a confidently-wrong caption is worse than none. pagespeak keeps the original image beside every block, biases the prompt toward caption-only when a figure isn't cleanly structural, and for critical content you should spot-check the Mermaid against the source rather than trust it blindly. The worked examples report the real per-figure hit rate (e.g. organs named for 7 of 11 body systems), not a perfect one.
120
+
121
+ ## Format support
122
+
123
+ | Format | Backend |
124
+ |---|---|
125
+ | `.pdf` | [Marker](https://github.com/VikParuchuri/marker) (default, fast) or [Docling](https://github.com/DS4SD/docling) (accuracy-first). See [docs/backends.md](docs/backends.md). |
126
+ | `.docx`, `.pptx`, `.xlsx`, `.html`, `.htm`, `.csv`, `.json`, `.xml`, `.epub` | [MarkItDown](https://github.com/microsoft/markitdown) |
127
+ | Canvas QTI quiz export (directory or `.imscc`) | Built-in QTI backend → one markdown file per quiz with the answer key. See [docs/canvas-quizzes.md](docs/canvas-quizzes.md). |
128
+ | Top Hat quiz-export PDF | `--pdf-backend tophat` → one `## Question N` block per question, correct answer marked when revealed, embedded figures extracted + captioned. See [docs/tophat-quizzes.md](docs/tophat-quizzes.md). |
129
+
130
+ ## How it relates to other tools
131
+
132
+ pagespeak is not a parser — it wraps existing extractors and runs cleanup, structuring, and diagram passes around their output.
133
+
134
+ | Tool | What it is | How pagespeak relates |
135
+ |---|---|---|
136
+ | [MarkItDown](https://github.com/microsoft/markitdown), [Marker](https://github.com/VikParuchuri/marker), [Docling](https://github.com/DS4SD/docling) | Open-source document → markdown extractors | Used as pagespeak's backends; pagespeak runs heading repair, section splitting, decoration stripping, and diagram→Mermaid on their output |
137
+ | LlamaParse, Reducto, Mathpix | Hosted, paid extraction APIs for complex documents | Different model — pagespeak runs locally on the extractors above, with optional $0 vision via Claude Code |
138
+ | Unstructured | Partitions documents into typed elements for RAG frameworks | Different output — pagespeak emits per-section markdown files with breadcrumbs and embedded Mermaid |
139
+
140
+ ### Why a layer at all — heading fidelity
141
+
142
+ The hardest part of PDF→markdown for RAG is the heading tree, because that's what chunking splits on. PDFs don't store semantic heading levels — only font sizes — so every extractor *guesses*, and each flattens or mis-levels real documents in a different way:
143
+
144
+ | | Heading hierarchy | Tables | Figures / formulas |
145
+ |---|---|---|---|
146
+ | **Marker** (default) | 4-level pyramid in single-shot; **flattens in the chunked pipeline** (per-chunk font stats disagree). MPS crash on Apple Silicon → `--device cpu` | occasionally collapses a multi-column table into one cell | — |
147
+ | **Docling** | **capped at 2 levels by design** — its layout model labels every section heading `level=1` | well-formed, TableFormer-grade | ~25% more figures on textbooks; formula → LaTeX |
148
+
149
+ So no backend simply gets structure right, and "just use Marker" or "just use Docling" inherits that backend's specific failure. Recovering the structure is pagespeak's reason to exist: an optional LLM heading-renormalization stage rebuilds a flattened hierarchy, deterministic post-passes repair levels at $0, and `repair-tables` re-reads *just* a collapsed table's page through Docling and splices the clean grid back into Marker's output — one page, not a second full conversion. Pick the backend for its strengths; pagespeak patches its known weakness. The full trade-off and recipes: [docs/backends.md](docs/backends.md) and [docs/choosing-defaults.md](docs/choosing-defaults.md).
150
+
151
+ ## Docs
152
+
153
+ - [docs/pipeline.md](docs/pipeline.md) — stage-by-stage walkthrough of what every command runs (spine)
154
+ - [docs/worked-examples.md](docs/worked-examples.md) — end-to-end before/after on real documents: extraction, repair, retrieval, and the cross-document payoff
155
+ - [docs/usage.md](docs/usage.md) — library + CLI examples, kwargs, env vars, common recipes
156
+ - [docs/choosing-defaults.md](docs/choosing-defaults.md) — pre-ingest triage: canonical recipe, vendor patterns, when to deviate
157
+ - [docs/presets.md](docs/presets.md) — config presets and `<output>/.pagespeak-run.json`
158
+ - [docs/architecture.md](docs/architecture.md) — module layout, data flow
159
+ - [docs/diagrams.md](docs/diagrams.md) — vision pass, prompt versioning
160
+ - [docs/cleanup.md](docs/cleanup.md) — cleanup levels, cross-refs, section splitting
161
+ - [docs/normalize-headings.md](docs/normalize-headings.md) — heading-level renormalization
162
+ - [docs/audit.md](docs/audit.md) — `pagespeak audit`: scan converted output for conversion defects (read-only, $0)
163
+ - [docs/repair-tables.md](docs/repair-tables.md) — `pagespeak repair-tables`: splice Docling's clean grid into Marker-collapsed tables (the fix for the audit's `collapsed_table`)
164
+ - [docs/caching.md](docs/caching.md) — cache layers, `--rerun-from`, baselines, diff
165
+ - [docs/backends.md](docs/backends.md) — Marker vs Docling for PDF
166
+ - [docs/docx-backends.md](docs/docx-backends.md) — MarkItDown vs python-docx for DOCX
167
+ - [docs/ingest.md](docs/ingest.md) — `pagespeak ingest`, chunked-parallel workers, resume semantics
168
+ - [docs/format-support.md](docs/format-support.md) — per-format quirks
169
+ - [docs/canvas-quizzes.md](docs/canvas-quizzes.md) — Canvas QTI quiz exports → one markdown file per quiz
170
+ - [docs/tophat-quizzes.md](docs/tophat-quizzes.md) — Top Hat quiz-export PDFs → per-question markdown (`--pdf-backend tophat`)
171
+ - [docs/operations.md](docs/operations.md) — sandbox / `ProcessPoolExecutor` gotchas
172
+ - [docs/web.md](docs/web.md) — web console: upload/queue, per-phase cockpit, cost gate, LLM observability
173
+
174
+ ## Security
175
+
176
+ [SECURITY.md](SECURITY.md) covers vulnerability reporting and safe-usage notes for shared environments (the console has no auth; remote-image fetching is SSRF-guarded).
177
+
178
+ ## License
179
+
180
+ MIT.
@@ -0,0 +1,126 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pagespeak"
7
+ version = "0.1.0"
8
+ description = "Convert PDFs and Office documents to LLM-friendly markdown, with diagram extraction to Mermaid."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Mike Farr" }]
14
+ keywords = ["pdf", "docx", "markdown", "llm", "rag", "ingest", "mermaid"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Text Processing :: Markup :: Markdown",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "anthropic>=0.39.0",
27
+ "beautifulsoup4>=4.12",
28
+ "httpx>=0.25",
29
+ "markdownify>=0.13",
30
+ "markitdown[docx,pptx,xlsx,outlook]>=0.0.1a3",
31
+ "pf-core[image-phash,tracking,llm]~=0.2.0",
32
+ "typer>=0.12.0",
33
+ ]
34
+ # beautifulsoup4 + markdownify back the inline-HTML→markdown utility
35
+ # (utils/_html.py, used by the QTI backend). Both were already present
36
+ # transitively via markitdown; declared directly since we import them.
37
+ # Note: ImageHash and Pillow used to be direct deps; now pulled in transitively
38
+ # via the pf-core[image-phash] extra.
39
+
40
+ [project.optional-dependencies]
41
+ pdf = [
42
+ "marker-pdf>=0.2.0",
43
+ "pypdfium2",
44
+ ]
45
+ pdf-docling = [
46
+ "docling>=2.0",
47
+ "pypdfium2",
48
+ ]
49
+ docx-structured = [
50
+ "python-docx>=1.1",
51
+ ]
52
+ tophat = [
53
+ # Top Hat quiz-export PDFs: text-layer extraction only (no Marker/torch).
54
+ # pypdfium2 also ships with the `pdf` / `pdf-docling` extras, so PDF users
55
+ # already have it; this light extra is for tophat-only installs.
56
+ "pypdfium2",
57
+ ]
58
+ web = [
59
+ "fastapi>=0.110",
60
+ "uvicorn[standard]>=0.29",
61
+ "jinja2>=3.1",
62
+ "python-multipart>=0.0.9",
63
+ ]
64
+ postgres = [
65
+ # Postgres driver for the LLM-call tracking DB. Default tracking sink
66
+ # is SQLite at `~/.pagespeak/llm_tracking.db` — install this extra +
67
+ # set `DATABASE_URL=postgresql://...` to point at a shared Postgres
68
+ # instance instead. SQLAlchemy 2.0 + pf-core's tracking schema work
69
+ # against either with no code changes.
70
+ "psycopg2-binary>=2.9",
71
+ ]
72
+ dev = [
73
+ "pytest>=8.0",
74
+ "pytest-mock>=3.12",
75
+ "ruff>=0.5",
76
+ "mypy>=1.10",
77
+ # Type stubs for transitively-imported third-party packages so mypy
78
+ # strict mode passes without per-import `type: ignore[import-untyped]`.
79
+ "types-PyYAML>=6.0",
80
+ # Runs the build gates (file-size + ruff) on every commit; the
81
+ # .github/workflows/guards.yml CI job is the unskippable backstop.
82
+ "pre-commit>=3.7",
83
+ ]
84
+
85
+ [project.scripts]
86
+ pagespeak = "pagespeak.cli:main"
87
+
88
+ [project.urls]
89
+ Homepage = "https://github.com/phierceweb/pagespeak"
90
+ Repository = "https://github.com/phierceweb/pagespeak"
91
+ Changelog = "https://github.com/phierceweb/pagespeak/blob/main/CHANGELOG.md"
92
+ Issues = "https://github.com/phierceweb/pagespeak/issues"
93
+
94
+ [tool.setuptools.packages.find]
95
+ where = ["src"]
96
+
97
+ [tool.setuptools.package-data]
98
+ pagespeak = ["py.typed", "prompts/*.yaml"]
99
+
100
+ [tool.ruff]
101
+ line-length = 100
102
+ target-version = "py311"
103
+
104
+ [tool.ruff.lint]
105
+ select = ["E", "F", "W", "I", "B", "UP", "SIM"]
106
+ ignore = ["E501"]
107
+
108
+ [tool.ruff.lint.per-file-ignores]
109
+ "src/pagespeak/cli/__init__.py" = ["B008"] # Typer uses function calls in argument defaults
110
+ "src/pagespeak/cli/_convert.py" = ["B008"] # Typer uses function calls in argument defaults
111
+ "src/pagespeak/cli/_invalidate.py" = ["B008"] # Typer uses function calls in argument defaults
112
+ "src/pagespeak/cli/_baseline.py" = ["B008"] # Typer uses function calls in argument defaults
113
+ "src/pagespeak/cli/_ingest.py" = ["B008"] # Typer uses function calls in argument defaults
114
+ "src/pagespeak/cli/_deliver.py" = ["B008"] # Typer uses function calls in argument defaults
115
+ "src/pagespeak/cli/_audit.py" = ["B008"] # Typer uses function calls in argument defaults
116
+ "src/pagespeak/cli/_repair.py" = ["B008"] # Typer uses function calls in argument defaults
117
+ "tests/*" = ["B011"]
118
+
119
+ [tool.mypy]
120
+ python_version = "3.11"
121
+ strict = true
122
+ ignore_missing_imports = true
123
+
124
+ [tool.pytest.ini_options]
125
+ testpaths = ["tests"]
126
+ addopts = "-x --ff -ra"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,68 @@
1
+ """pagespeak — convert documents to LLM-friendly markdown with diagram extraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os as _os
6
+
7
+ from pf_core.log import setup_logging as _setup_logging
8
+
9
+ # Configure pf-core's structlog bridge BEFORE any sub-imports below: those
10
+ # imports load library modules whose top-level `logger = get_logger(__name__)`
11
+ # calls trigger pf-core's `setup_logging()` lazily with default
12
+ # `app_logger_name="app"`. pf-core guards re-entry via a `_setup_done` flag,
13
+ # so whichever caller runs first wins. Doing it here — at the package root —
14
+ # means whether pagespeak is invoked via the CLI entry point (which imports
15
+ # `pagespeak.cli`, transitively triggering this) or imported as a library
16
+ # (`from pagespeak import to_markdown`), the handler attaches to the
17
+ # `pagespeak` logger root so module-level loggers
18
+ # (`pagespeak.services._diagrams`, etc.) propagate to it.
19
+ #
20
+ # Library users who want different logging behavior can call
21
+ # `pf_core.log.setup_logging(...)` themselves BEFORE `import pagespeak`.
22
+ _setup_logging(
23
+ level=_os.environ.get("PAGESPEAK_LOG_LEVEL"),
24
+ app_logger_name="pagespeak",
25
+ )
26
+
27
+ from .backends._pdf_dispatch import PdfBackendName # noqa: E402
28
+ from .models._models import Diagram, IngestResult # noqa: E402
29
+ from .models._pipeline import ChunkState, Manifest, VisionState # noqa: E402
30
+ from .orchestrators._chunk import chunk # noqa: E402
31
+ from .orchestrators._dispatch import to_markdown # noqa: E402
32
+ from .orchestrators._ingest import ingest # noqa: E402
33
+ from .services._cleanup import CleanupLevel, CrossRefs # noqa: E402
34
+ from .services._diagrams import ( # noqa: E402
35
+ VisionBackendName,
36
+ gather_diagrams,
37
+ inject_diagrams,
38
+ )
39
+ from .services._heading_normalize import ( # noqa: E402
40
+ NormalizeData,
41
+ NormalizeMode,
42
+ apply_normalization,
43
+ gather_normalize_levels,
44
+ )
45
+ from .services._rerun import RERUN_STAGES # noqa: E402
46
+
47
+ __version__ = "0.1.0"
48
+ __all__ = [
49
+ "ChunkState",
50
+ "CleanupLevel",
51
+ "CrossRefs",
52
+ "Diagram",
53
+ "IngestResult",
54
+ "Manifest",
55
+ "NormalizeData",
56
+ "NormalizeMode",
57
+ "PdfBackendName",
58
+ "RERUN_STAGES",
59
+ "VisionBackendName",
60
+ "VisionState",
61
+ "apply_normalization",
62
+ "chunk",
63
+ "gather_diagrams",
64
+ "gather_normalize_levels",
65
+ "inject_diagrams",
66
+ "ingest",
67
+ "to_markdown",
68
+ ]