Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

536 lines
23KB

  1. """Export a book PDF to text and split it into per-chapter source-text.md files.
  2. Usage: python tools/split_book.py "<Book Title>"
  3. Reads library/<Book Title>/source-file.pdf and writes
  4. library/<Book Title>/<Chapter-XX-Title-Slug>/<Chapter-XX>-source-text.md for each section defined in SECTIONS
  5. below (add an entry for a new book). Sections are (folder, title, first_page, last_page,
  6. note) with 1-based PDF pages. Pages not covered by any section are reported.
  7. """
  8. import json
  9. import re
  10. import sys
  11. from pathlib import Path
  12. import fitz # PyMuPDF
  13. ROOT = Path(__file__).resolve().parent.parent / "library"
  14. YBYE_NOTE_AFTER = "Includes the Step action plan that closes this step."
  15. YBYE_NOTE_BEFORE = "Includes the Step introduction page that precedes the chapter."
  16. def ch(n):
  17. return f"Chapter-{n:02d}"
  18. def slug(title, limit=60):
  19. """Title -> folder-name slug (see AGENTS.md Naming Convention)."""
  20. t = re.sub(r"^\d+(\.\d+)?[a-z]?\s*-\s*", "", title.strip()) # drop the book's own numbering prefix
  21. t = re.sub(r"[:,.'\"‘’“”?/\()]", "", t)
  22. t = re.sub(r"[^\w\s-]", "", t)
  23. words, out = t.split(), ""
  24. for w in words:
  25. if len(out) + len(w) + (1 if out else 0) > limit:
  26. break
  27. out = f"{out}-{w}" if out else w
  28. return re.sub(r"-+", "-", out) or "Untitled"
  29. def folder_name(key, title):
  30. """Chapter-05 + title -> Chapter-05-Anatomy-of-a-Specification; Front/Back-Matter keep their name."""
  31. return key if key in ("Front-Matter", "Back-Matter") else f"{key}-{slug(title)}"
  32. def build_cetx():
  33. d = fitz.open(ROOT / "Context Engineering" / "source-file.pdf")
  34. top = [(t, p) for lvl, t, p in d.get_toc() if lvl == 1]
  35. secs = [("Front-Matter", "Front matter", 1, top[0][1] - 1, "")]
  36. for i, (t, p) in enumerate(top):
  37. end = (top[i + 1][1] - 1) if i + 1 < len(top) else len(d)
  38. secs.append((ch(i + 1), t, p, end, ""))
  39. return secs
  40. def build_12wy():
  41. d = fitz.open(ROOT / "The 12 Week Year" / "source-file.pdf")
  42. starts = []
  43. for lvl, t, p in d.get_toc():
  44. if t.startswith("Chapter ") and lvl in (1, 2):
  45. num = int(t.split(":")[0].split()[1])
  46. starts.append((num, t.split(":", 1)[1].strip(), p))
  47. secs = [("Front-Matter", "Front matter", 1, starts[0][2] - 1, "")]
  48. for i, (n, t, p) in enumerate(starts):
  49. end = starts[i + 1][2] - 1 if i + 1 < len(starts) else 168
  50. note = ""
  51. if n == 1:
  52. note = "Includes the Part I divider page that follows the chapter."
  53. secs.append((ch(n), t, p, end, note))
  54. secs.append(("Back-Matter", "References", 169, len(d), ""))
  55. return secs
  56. def build_ybye():
  57. chapters = [ # (n, title, first, last, note)
  58. (0, "Your Best Is Yet to Come (Opening)", 11, 21, ""),
  59. (1, "Your Beliefs Shape Your Reality", 22, 29, YBYE_NOTE_BEFORE),
  60. (2, "Some Beliefs Hold You Back", 30, 38, ""),
  61. (3, "You Can Upgrade Your Beliefs", 39, 53, YBYE_NOTE_AFTER),
  62. (4, "Thinking Backward Is a Must", 54, 64, YBYE_NOTE_BEFORE),
  63. (5, "Regret Reveals Opportunity", 65, 73, ""),
  64. (6, "Gratitude Makes the Difference", 74, 82, YBYE_NOTE_AFTER),
  65. (7, "Great Goals Check Seven Boxes", 83, 102, YBYE_NOTE_BEFORE),
  66. (8, "Achievements and Habits Work Together", 103, 109, ""),
  67. (9, "Seriously, Risk Is Your Friend", 110, 124, YBYE_NOTE_AFTER),
  68. (10, "Your What Needs a Why", 125, 139, YBYE_NOTE_BEFORE),
  69. (11, "You Can Master Your Own Motivation", 140, 148, ""),
  70. (12, "The Journey Is Better with Friends", 149, 160, YBYE_NOTE_AFTER),
  71. (13, "One Journey Is Many Steps", 161, 170, YBYE_NOTE_BEFORE),
  72. (14, "You Can Trigger Success", 171, 177, ""),
  73. (15, "Visibility Is Essential", 178, 190, YBYE_NOTE_AFTER),
  74. ]
  75. secs = [("Front-Matter", "Front matter (title, copyright, endorsements, contents)", 1, 10, "")]
  76. secs += [(ch(n), t, a, b, note) for n, t, a, b, note in chapters]
  77. secs.append(("Back-Matter", "LEAP Principle, Sample Goal Templates, Notes, Thanks, Index, About the Author", 191, 235, ""))
  78. return secs # pages 236-242 (ads, back cover) are intentionally excluded
  79. def parse_table(book, first_col_prefix, skip_rows=0):
  80. rows = []
  81. for line in (ROOT / book / "book-structure.md").read_text(encoding="utf-8").splitlines():
  82. if line.startswith("|") and not line.startswith("| ---") and not line.startswith("| Reading"):
  83. rows.append([c.strip() for c in line.strip("|").split("|")])
  84. return rows
  85. def build_sdd():
  86. d = fitz.open(ROOT / "Spec Driven Development" / "source-file.pdf")
  87. secs = [("Front-Matter", "Front matter (cover, title, contents)", 1, 10, "")]
  88. for r in parse_table("Spec Driven Development", ""):
  89. n = int(r[0].split("-")[1])
  90. a, b = r[1].replace("–", "-").split("-")
  91. secs.append((ch(n), r[2], int(a), int(b), ""))
  92. return secs
  93. def build_focus():
  94. d = fitz.open(ROOT / "FOCUS Architecture" / "source-file.pdf")
  95. rows = []
  96. for r in parse_table("FOCUS Architecture", ""):
  97. rows.append((r[0], r[1], int(r[2])))
  98. secs = [("Front-Matter", "Front matter", 1, rows[0][2] - 1, "")]
  99. for i, (kind, title, start) in enumerate(rows):
  100. end = rows[i + 1][2] - 1 if i + 1 < len(rows) else len(d)
  101. folder = "Interlude" if kind == "Interlude" else ch(int(kind.split()[1]))
  102. secs.append((folder, title, start, end, ""))
  103. return secs
  104. def build_inference():
  105. d = fitz.open(ROOT / "Inference Engineering" / "source-file.pdf")
  106. top = [(t, p) for lvl, t, p in d.get_toc() if lvl <= 2]
  107. # Skip the Table of Contents bookmark: it is front matter (pages 1-10 with the cover and TOC).
  108. top = [(t, p) for t, p in top if t != "Table of Contents"]
  109. last_body = next(p for t, p in top if t == "Acknowledgements")
  110. secs = [("Front-Matter", "Front matter (cover, title, contents)", 1, top[0][1] - 1, "")]
  111. body = [(t, p) for t, p in top if p < last_body]
  112. for i, (t, p) in enumerate(body):
  113. end = (body[i + 1][1] - 1) if i + 1 < len(body) else last_body - 1
  114. title = re.sub(r"^Chapter \d+:\s*", "", t)
  115. secs.append((ch(i + 1), title, p, end, ""))
  116. secs.append(("Back-Matter", "Acknowledgements", last_body, len(d), ""))
  117. return secs
  118. def build_os32():
  119. # Scanned book (no bookmarks): printed page N is PDF page N + 22. Chapters that open a Part
  120. # take in the Part divider plate and its blank verso, two pages before the chapter plate.
  121. chapters = [ # (n, title, printed start page, part that opens here)
  122. (1, "Overview", 3, "I"),
  123. (2, "General Discussion and Background", 13, ""),
  124. (3, "The Tasking Model", 25, "II"),
  125. (4, "Interprocess Communications", 41, ""),
  126. (5, "Memory Management", 55, ""),
  127. (6, "The Hardware Interface", 81, ""),
  128. (7, "OS Initialization", 99, ""),
  129. (8, "Programming Interfaces", 115, ""),
  130. (9, "Application Programming", 129, "III"),
  131. (10, "Systems Programming", 141, ""),
  132. (11, "The MMURTL Monitor Program", 159, ""),
  133. (12, "The Debugger", 181, ""),
  134. (13, "Keyboard Service", 189, ""),
  135. (14, "File System Service", 201, ""),
  136. (15, "API Specification", 221, ""),
  137. (16, "MMURTL Sample Software", 265, ""),
  138. (17, "Introduction to the Source Code", 327, "IV"),
  139. (18, "Kernel Code", 335, ""),
  140. (19, "Memory Management Code", 377, ""),
  141. (20, "Timer Management Code", 405, ""),
  142. (21, "Initialization Code", 419, ""),
  143. (22, "Job Management Code", 439, ""),
  144. (23, "Debugger Code", 471, ""),
  145. (24, "Selected Device Driver Code", 497, ""),
  146. (25, "Keyboard Code", 541, ""),
  147. (26, "Video Code", 571, ""),
  148. (27, "File System Code", 591, ""),
  149. (28, "DASM: A 32-Bit Intel-Based Assembler", 663, "V"),
  150. (29, "CM32: A 32-Bit C Compiler", 697, ""),
  151. ]
  152. index_start = 719 + 22
  153. starts = [(n, t, s + 22 - (2 if part else 0), part) for n, t, s, part in chapters]
  154. secs = [("Front-Matter", "Front matter (covers, title, copyright, contents, preface, acknowledgments, about the author)", 1, starts[0][2] - 1, "")]
  155. for i, (n, t, a, part) in enumerate(starts):
  156. end = starts[i + 1][2] - 1 if i + 1 < len(starts) else index_start - 1
  157. note = f"Includes the Part {part} divider plate and its blank verso before the chapter." if part else ""
  158. secs.append((ch(n), t, a, end, note))
  159. secs.append(("Back-Matter", "Index, publisher ads, CD-ROM installation and contents, back cover", index_start, 772, ""))
  160. return secs
  161. def build_three_laws():
  162. # The PDF has a reliable bookmark outline. Part divider pages are folded into
  163. # the chapter that follows them so every page belongs to exactly one section.
  164. d = fitz.open(ROOT / "The Three Laws of Performance" / "source-file.pdf")
  165. chapters = [ # (number, title, first PDF page)
  166. (1, "Transforming an Impossible Situation", 40),
  167. (2, "Where Is the Key to Performance?", 70),
  168. (3, "Rewriting a Future That's Already Written", 104),
  169. (4, "With So Many Books on Leadership, Why Are There So Few Leaders?", 130),
  170. (5, "The Self-Led Organization", 156),
  171. (6, "Who or What Is Leading Your Life?", 180),
  172. (7, "The Path to Mastery", 208),
  173. (8, "Breaking the Performance Barrier", 238),
  174. ]
  175. secs = [(
  176. "Front-Matter",
  177. "Front matter (cover, title, contents, notes, foreword, and introduction)",
  178. 1,
  179. chapters[0][2] - 1,
  180. "",
  181. )]
  182. for i, (n, title, start) in enumerate(chapters):
  183. end = chapters[i + 1][2] - 1 if i + 1 < len(chapters) else 245
  184. part = {1: "I", 4: "II", 6: "III"}.get(n)
  185. note = f"Includes the Part {part} divider immediately before the chapter." if part else ""
  186. secs.append((ch(n), title, start, end, note))
  187. secs.append((
  188. "Back-Matter",
  189. "Appendix, notes, acknowledgments, author biographies, and index",
  190. 246,
  191. len(d),
  192. "",
  193. ))
  194. return secs
  195. def build_smart_calling():
  196. # Section divider pages are folded into the chapter that follows them.
  197. d = fitz.open(ROOT / "Smart Calling" / "source-file.pdf")
  198. chapters = []
  199. for level, title, page in d.get_toc():
  200. if level == 3 and title.startswith("Chapter "):
  201. match = re.match(r"Chapter (\d+):\s*(.*)", title)
  202. if match:
  203. chapters.append((int(match.group(1)), match.group(2).strip(), page))
  204. section_dividers = {2: 29, 7: 93, 17: 207}
  205. starts = [
  206. (number, title, section_dividers.get(number, page))
  207. for number, title, page in chapters
  208. ]
  209. secs = [(
  210. "Front-Matter",
  211. "Front matter and introduction",
  212. 1,
  213. starts[0][2] - 1,
  214. "",
  215. )]
  216. for i, (number, title, start) in enumerate(starts):
  217. end = starts[i + 1][2] - 1 if i + 1 < len(starts) else 240
  218. section = {2: "Two", 7: "Three", 17: "Four"}.get(number)
  219. note = f"Includes the Section {section} divider immediately before the chapter." if section else ""
  220. secs.append((ch(number), title, start, end, note))
  221. secs.append(("Back-Matter", "Index and About Art Sobczak", 241, len(d), ""))
  222. return secs
  223. def build_fanatical_prospecting():
  224. d = fitz.open(ROOT / "Fanatical Prospecting" / "source-file.pdf")
  225. chapters = []
  226. for level, title, page in d.get_toc():
  227. if level == 1 and title.startswith("Chapter "):
  228. match = re.match(r"Chapter (\d+)\s+(.*)", title)
  229. if match:
  230. chapters.append((int(match.group(1)), match.group(2).strip(), page))
  231. secs = [(
  232. "Front-Matter",
  233. "Front matter, foreword, and free prospecting resources note",
  234. 1,
  235. chapters[0][2] - 1,
  236. "",
  237. )]
  238. for i, (number, title, start) in enumerate(chapters):
  239. end = chapters[i + 1][2] - 1 if i + 1 < len(chapters) else 288
  240. secs.append((ch(number), title, start, end, ""))
  241. secs.append((
  242. "Back-Matter",
  243. "Notes, acknowledgments, about the author, index, and back cover",
  244. 289,
  245. len(d),
  246. "",
  247. ))
  248. return secs
  249. def build_ogilvy_on_advertising():
  250. d = fitz.open(ROOT / "Ogilvy on Advertising" / "source-file.pdf")
  251. chapters = []
  252. back_start = None
  253. for level, title, page in d.get_toc():
  254. match = re.match(r"(\d+)\.\s+(.*)", title)
  255. if level == 1 and match:
  256. chapters.append((int(match.group(1)), match.group(2).strip(), page))
  257. elif level == 1 and title == "Useful reference sources":
  258. back_start = page
  259. if not chapters or back_start is None:
  260. raise SystemExit("Could not locate the Ogilvy chapter or back-matter bookmarks")
  261. secs = [(
  262. "Front-Matter",
  263. "Front matter, publisher's note, and contents",
  264. 1,
  265. chapters[0][2] - 1,
  266. "",
  267. )]
  268. for i, (number, title, start) in enumerate(chapters):
  269. end = chapters[i + 1][2] - 1 if i + 1 < len(chapters) else back_start - 1
  270. secs.append((ch(number), title, start, end, ""))
  271. secs.append((
  272. "Back-Matter",
  273. "Useful reference sources, index, picture credits, and copyright",
  274. back_start,
  275. len(d),
  276. "",
  277. ))
  278. return secs
  279. def build_direct_mail_solution():
  280. d = fitz.open(ROOT / "The Direct Mail Solution" / "source-file.pdf")
  281. chapters = []
  282. back_start = None
  283. for level, title, page in d.get_toc():
  284. match = re.match(r"Chapter (\d+):\s*(.*)", title)
  285. if level == 1 and match:
  286. chapters.append((int(match.group(1)), match.group(2).strip(), page))
  287. elif level == 1 and title == "About the Authors":
  288. back_start = page
  289. if not chapters or back_start is None:
  290. raise SystemExit("Could not locate The Direct Mail Solution chapter or back-matter bookmarks")
  291. secs = [(
  292. "Front-Matter",
  293. "Front matter, acknowledgements, and preface",
  294. 1,
  295. chapters[0][2] - 1,
  296. "",
  297. )]
  298. for i, (number, title, start) in enumerate(chapters):
  299. end = chapters[i + 1][2] - 1 if i + 1 < len(chapters) else back_start - 1
  300. secs.append((ch(number), title, start, end, ""))
  301. secs.append((
  302. "Back-Matter",
  303. "About the authors, index, and book excerpt",
  304. back_start,
  305. len(d),
  306. "",
  307. ))
  308. return secs
  309. def build_hype_handbook():
  310. d = fitz.open(ROOT / "The Hype Handbook" / "source-file.pdf")
  311. chapters = []
  312. back_start = None
  313. for level, title, page in d.get_toc():
  314. match = re.match(r"Hype Strategy #(\d+)\s+(.*)", title)
  315. if level == 1 and match:
  316. chapters.append((int(match.group(1)), match.group(2).strip(), page))
  317. elif level == 1 and title == "Epilogue":
  318. back_start = page
  319. if not chapters or back_start is None:
  320. raise SystemExit("Could not locate The Hype Handbook strategy or back-matter bookmarks")
  321. secs = [(
  322. "Front-Matter",
  323. "Front matter, acknowledgments, and introduction",
  324. 1,
  325. chapters[0][2] - 1,
  326. "",
  327. )]
  328. for i, (number, title, start) in enumerate(chapters):
  329. end = chapters[i + 1][2] - 1 if i + 1 < len(chapters) else back_start - 1
  330. secs.append((ch(number), title, start, end, ""))
  331. secs.append((
  332. "Back-Matter",
  333. "Epilogue, bibliography, and index",
  334. back_start,
  335. len(d),
  336. "",
  337. ))
  338. return secs
  339. def build_code_of_discipline():
  340. # Photocopied scan (no bookmarks); the scanner's OCR text layer is used as is. Part divider
  341. # pages are folded into the chapter that follows them. The book-level Introduction is
  342. # Chapter 00 and the book-level Conclusion is Chapter 10.
  343. chapters = [ # (n, title, first PDF page, part that opens here)
  344. (0, "Introduction: The Discipline Crisis", 5, ""),
  345. (1, "The War Against Comfort", 16, "1"),
  346. (2, "Grit Over Feelings", 32, ""),
  347. (3, "The Focus Formula", 46, ""),
  348. (4, "The Mental Resistance Trap", 59, "2"),
  349. (5, "The Science of Self-Discipline", 73, ""),
  350. (6, "Mastering the Morning Battlefield", 86, ""),
  351. (7, "The Discipline Code Framework", 99, "3"),
  352. (8, "The 90-Day Mental Toughness Protocol", 115, ""),
  353. (9, "The Discipline Code in Work, Fitness, and Life", 128, ""),
  354. (10, "Conclusion: Becoming a Person of Unbreakable Will", 141, ""),
  355. ]
  356. secs = [("Front-Matter", "Front matter (cover, inside cover, contents)", 1, 4, "")]
  357. for i, (n, title, start, part) in enumerate(chapters):
  358. end = chapters[i + 1][2] - 1 if i + 1 < len(chapters) else 153
  359. note = "Text is the scanner's OCR layer; expect character errors (e.g. 'Thaining', 'joumey'), dropped drop-cap letters, and OCR-mangled headings."
  360. if part:
  361. note += f" Includes the Part {part} divider immediately before the chapter."
  362. secs.append((ch(n), title, start, end, note))
  363. secs.append(("Back-Matter", "Blank pages (About the Author page not scanned) and back cover", 154, 156, ""))
  364. return secs
  365. WATTLES_WORKS = [ # (title bookmark, short name)
  366. ("The Science of Getting Rich", "The Science of Getting Rich"),
  367. ("The Science of Being Well", "The Science of Being Well"),
  368. ("The Science of Being Great", "The Science of Being Great"),
  369. ("Hellfire Harrison (A Novel)", "Hellfire Harrison"),
  370. ("Jesus: The Man and His Work", "Jesus: The Man and His Work"),
  371. ("A New Christ", "A New Christ"),
  372. ("How to Get What You Want", "How to Get What You Want"),
  373. ("Making of the Man Who Can", "Making of the Man Who Can"),
  374. ("New Science of Living and Healing", "New Science of Living and Healing"),
  375. ("The Personal Power Course", "The Personal Power Course"),
  376. ]
  377. WATTLES_NOVEL_ADS = ["Commencement Days", "The Court of Lucifer", "Ailmeny of the Orchard",
  378. "A Cavalier of Virginia", "The Lead of honour", "My heart and Stephanie"]
  379. def wattles_title(work, t):
  380. """Bookmark title -> chapter title; generic 'Chapter One' titles get the work's name."""
  381. t = re.sub(r"^Chapter [IVXL]+\.\s*", "", t) # Science of Being Well
  382. t = re.sub(r"^Chapter \d+\s*[:-]\s*", "", t) # Getting Rich, A New Christ, Man Who Can
  383. t = re.sub(r"^Lesson (\d+)\s+", r"Lesson \1 ", t) # Personal Power Course
  384. m = re.match(r"^\d+\.\s*(.*?)\.?$", t) # New Science of Living: '1. THE SOURCE OF WORK-POWER.'
  385. if m:
  386. t = re.sub(r"(?<=\s)(Of|And|In|On|To)\b", lambda w: w.group(1).lower(), m.group(1).title())
  387. if re.fullmatch(r"Chapter (\w+)", t):
  388. t = f"{work} {t}"
  389. return t.replace("Jesus�", "Jesus'")
  390. def build_wattles():
  391. # One book folder for the whole collection, chapters numbered continuously across the ten
  392. # works. Each work's title page (and preface, where present) is folded into its first chapter.
  393. d = fitz.open(ROOT / "Wallace D. Wattles Ultimate Collection" / "source-file.pdf")
  394. toc = [(t, p) for lvl, t, p in d.get_toc() if lvl == 1 and p >= 5]
  395. chapters, work, pending_start = [], None, None
  396. for t, p in toc:
  397. new_work = next((s for b, s in WATTLES_WORKS if t.startswith(b)), None)
  398. if new_work:
  399. work, pending_start = new_work, p
  400. if work == "Jesus: The Man and His Work": # a single-section work
  401. chapters.append((work, work, p))
  402. pending_start = None
  403. continue
  404. if t == "Preface" or t in WATTLES_NOVEL_ADS:
  405. continue
  406. chapters.append((work, wattles_title(work, t), pending_start or p))
  407. pending_start = None
  408. counts = {w: sum(1 for c in chapters if c[0] == w) for _, w in WATTLES_WORKS}
  409. secs = [("Front-Matter", "Cover, title page, and collection contents", 1, 4, "")]
  410. seen = {}
  411. for i, (w, title, start) in enumerate(chapters):
  412. end = chapters[i + 1][2] - 1 if i + 1 < len(chapters) else len(d)
  413. seen[w] = seen.get(w, 0) + 1
  414. note = f"Work: *{w}*, section {seen[w]} of {counts[w]}."
  415. if seen[w] == 1 and counts[w] > 1:
  416. note += " Includes the work's title page" + (" and Preface" if w in ("The Science of Getting Rich", "The Science of Being Well") else "") + "."
  417. if w == "Hellfire Harrison" and seen[w] == counts[w]:
  418. note += " Includes the publisher's advertisements for other novels (PDF pages 255-261) that close the novel."
  419. secs.append((ch(i + 1), title, start, end, note))
  420. return secs
  421. BUILDERS = {
  422. "Developing Your Own 32-Bit Operating System": build_os32,
  423. "Context Engineering": build_cetx,
  424. "The 12 Week Year": build_12wy,
  425. "Your Best Year Ever": build_ybye,
  426. "Spec Driven Development": build_sdd,
  427. "FOCUS Architecture": build_focus,
  428. "Inference Engineering": build_inference,
  429. "The Three Laws of Performance": build_three_laws,
  430. "Smart Calling": build_smart_calling,
  431. "Fanatical Prospecting": build_fanatical_prospecting,
  432. "Ogilvy on Advertising": build_ogilvy_on_advertising,
  433. "The Direct Mail Solution": build_direct_mail_solution,
  434. "The Hype Handbook": build_hype_handbook,
  435. "The Code of Discipline": build_code_of_discipline,
  436. "Wallace D. Wattles Ultimate Collection": build_wattles,
  437. }
  438. def main(book):
  439. pdf = ROOT / book / "source-file.pdf"
  440. doc = fitz.open(pdf)
  441. secs = BUILDERS[book]()
  442. ocr_path = ROOT / book / "ocr-image-pages.json"
  443. ocr_pages = json.loads(ocr_path.read_text(encoding="utf-8")) if ocr_path.exists() else {}
  444. covered = {}
  445. for folder, title, a, b, note in secs:
  446. pages, blank = [], []
  447. for p in range(a, b + 1):
  448. if p in covered:
  449. raise SystemExit(f"page {p} in both {covered[p]} and {folder}")
  450. covered[p] = folder
  451. text = doc[p - 1].get_text().strip()
  452. if not text:
  453. blank.append(p)
  454. if str(p) in ocr_pages:
  455. text = "[OCR from image-only PDF page; verify layout and illustrations in the PDF.]\n\n" + ocr_pages[str(p)]
  456. pages.append(f"<!-- PDF page {p} -->\n{text}\n")
  457. header = [
  458. f"# {book} — {folder}: {title}",
  459. f"- **Source**: /library/{book}/source-file.pdf",
  460. f"- **PDF pages**: {a}–{b}",
  461. f"- **Pages without text**: {', '.join(map(str, blank)) if blank else 'none'}",
  462. ]
  463. if blank and ocr_pages:
  464. header.append("- **OCR note**: OCR is supplied below for image-only pages; multi-column reading order may be imperfect, so consult the PDF for layout and visuals.")
  465. if note:
  466. header.append(f"- **Note**: {note}")
  467. out = ROOT / book / folder_name(folder, title)
  468. out.mkdir(exist_ok=True)
  469. (out / f"{folder}-source-text.md").write_text("\n".join(header) + "\n\n---\n\n" + "\n".join(pages), encoding="utf-8", newline="\n")
  470. print(f"{folder:14} pp {a}-{b} blank={blank} {title[:50]}")
  471. missing = [p for p in range(1, len(doc) + 1) if p not in covered]
  472. print("pages not covered:", missing)
  473. if __name__ == "__main__":
  474. main(sys.argv[1])

Powered by TurnKey Linux.